From 0574d5dc551580837b359824d600c654c5f866c3 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Tue, 11 Jul 2023 17:01:53 +0100 Subject: [PATCH 01/90] Add default.nix --- cabal.project | 6 ++++++ default.nix | 2 ++ ndarray.cabal | 23 +++++++++++++++++++++++ ndarray.nix | 8 ++++++++ nixpkgs.nix | 3 +++ shell.nix | 1 + sources.json | 12 ++++++++++++ 7 files changed, 55 insertions(+) create mode 100644 cabal.project create mode 100644 default.nix create mode 100644 ndarray.cabal create mode 100644 ndarray.nix diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..ec23658 --- /dev/null +++ b/cabal.project @@ -0,0 +1,6 @@ +packages: . + +source-repository-package + type: git + location: https://github.com/cchalmers/dense.git + tag: 6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262 diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..2cbb352 --- /dev/null +++ b/default.nix @@ -0,0 +1,2 @@ +{ nixpkgs ? import ./nixpkgs.nix {} }: +nixpkgs.pkgs.haskellPackages.callPackage ./ndarray.nix { } diff --git a/ndarray.cabal b/ndarray.cabal new file mode 100644 index 0000000..e9c440b --- /dev/null +++ b/ndarray.cabal @@ -0,0 +1,23 @@ +cabal-version: 2.4 +name: ndarray +version: 0.1.0.0 +-- synopsis: +-- description: +license: MIT +license-file: LICENSE +author: Rowan Mather +maintainer: rowan@myrtle.ai +-- copyright: +build-type: Simple +extra-doc-files: CHANGELOG.md +-- extra-source-files: + +library + exposed-modules: MyLib + -- other-modules: + -- other-extensions: + build-depends: base >=4.13.0.0 + , dense + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: -Wall diff --git a/ndarray.nix b/ndarray.nix new file mode 100644 index 0000000..cfbd8c7 --- /dev/null +++ b/ndarray.nix @@ -0,0 +1,8 @@ +{ mkDerivation, base, dense, lib }: +mkDerivation { + pname = "ndarray"; + version = "0.1.0.0"; + src = ./.; + libraryHaskellDepends = [ base dense ]; + license = lib.licenses.mit; +} diff --git a/nixpkgs.nix b/nixpkgs.nix index ef19037..31a7851 100644 --- a/nixpkgs.nix +++ b/nixpkgs.nix @@ -6,6 +6,9 @@ let clash-compiler = import sources.clash-compiler {}; niv = (import sources.niv {}).niv; haskellPackages = pkgs.haskellPackages.override { overrides = self: super: { + dense = pkgs.haskell.lib.markUnbroken (pkgs.haskell.lib.overrideSrc super.dense { + src = sources.dense; + }); yoda = pkgs.haskell.lib.doJailbreak (self.callCabal2nix "yoda" sources.yoda {}); diff --git a/shell.nix b/shell.nix index 0c88006..278cf46 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,7 @@ let ghc = haskellPackages.ghcWithPackages (pkgs: with pkgs; [ QuickCheck ghcid repa + dense ]); in mkShell { diff --git a/sources.json b/sources.json index ecb4275..322e8fc 100644 --- a/sources.json +++ b/sources.json @@ -154,5 +154,17 @@ "type": "tarball", "url": "https://github.com/jaspervdj/stylish-haskell/archive/4ec5c509290d8c2b94045189a122d3fca5e45a4e.tar.gz", "url_template": "https://github.com///archive/.tar.gz" + }, + "dense": { + "branch": "master", + "description": "dense Haskell library", + "homepage": "", + "owner": "cchalmers", + "repo": "dense", + "rev": "a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa", + "sha256": "1rg7s1ybdac6139j8l511x2f9jg5793wr20vph54j9p6ybybigsd", + "type": "tarball", + "url": "https://github.com/cchalmers/dense/archive/a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" } } \ No newline at end of file From ef505549e82276b29ebbee9ccb8259c6d7cf9d4b Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:02:38 +0100 Subject: [PATCH 02/90] Bump dense Nix to most recent --- default.nix | 2 +- nixpkgs.nix => nix/nixpkgs.nix | 4 +- nix/sources.json | 62 ++++++++++++ sources.nix => nix/sources.nix | 0 shell.nix | 2 +- sources.json | 170 --------------------------------- 6 files changed, 66 insertions(+), 174 deletions(-) rename nixpkgs.nix => nix/nixpkgs.nix (88%) create mode 100644 nix/sources.json rename sources.nix => nix/sources.nix (100%) delete mode 100644 sources.json diff --git a/default.nix b/default.nix index 2cbb352..579d269 100644 --- a/default.nix +++ b/default.nix @@ -1,2 +1,2 @@ -{ nixpkgs ? import ./nixpkgs.nix {} }: +{ nixpkgs ? import nix/nixpkgs.nix {} }: nixpkgs.pkgs.haskellPackages.callPackage ./ndarray.nix { } diff --git a/nixpkgs.nix b/nix/nixpkgs.nix similarity index 88% rename from nixpkgs.nix rename to nix/nixpkgs.nix index 31a7851..d6a9a78 100644 --- a/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -6,9 +6,9 @@ let clash-compiler = import sources.clash-compiler {}; niv = (import sources.niv {}).niv; haskellPackages = pkgs.haskellPackages.override { overrides = self: super: { - dense = pkgs.haskell.lib.markUnbroken (pkgs.haskell.lib.overrideSrc super.dense { + dense = pkgs.haskell.lib.markUnbroken (pkgs.haskell.lib.dontCheck (pkgs.haskell.lib.overrideSrc super.dense { src = sources.dense; - }); + })); yoda = pkgs.haskell.lib.doJailbreak (self.callCabal2nix "yoda" sources.yoda {}); diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..c0fd4fd --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,62 @@ +{ + "clash-compiler": { + "branch": "1.4", + "description": "Haskell to VHDL/Verilog/SystemVerilog compiler", + "homepage": "https://www.clash-lang.org/", + "owner": "clash-lang", + "repo": "clash-compiler", + "rev": "fa01fd98799cd7c00cae44a9df847142410f1618", + "sha256": "0b42gschkb7sk958a7j529lcw9kmzikhcqjxblm43lggwwy24hns", + "type": "tarball", + "url": "https://github.com/clash-lang/clash-compiler/archive/fa01fd98799cd7c00cae44a9df847142410f1618.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "niv": { + "branch": "master", + "description": "Easy dependency management for Nix projects", + "homepage": "https://github.com/nmattia/niv", + "owner": "nmattia", + "repo": "niv", + "rev": "099f9ea92169c2edbc3ca33313b68f5e9686e800", + "sha256": "1rqaw2r5wbbfpbxjgwrwzf619la1iqvzp3mhq1mfz9vk4hjd3335", + "type": "tarball", + "url": "https://github.com/nmattia/niv/archive/099f9ea92169c2edbc3ca33313b68f5e9686e800.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs": { + "branch": "nixos-20.09", + "description": "Nix Packages collection", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "896270d629efd47d14972e96f4fbb79fc9f45c80", + "sha256": "0xmjjayg19wm6cn88sh724mrsdj6mgrql6r3zc0g4x9bx4y342p7", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/896270d629efd47d14972e96f4fbb79fc9f45c80.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "revealjs": { + "branch": "3.8.0", + "description": "The HTML Presentation Framework", + "homepage": "https://revealjs.com", + "owner": "hakimel", + "repo": "reveal.js", + "rev": "3da09f1fef1fa3c140d2daa5bdee2a32683dd964", + "sha256": "14cva2hxdv4gxpz2a996qs8xhxffw97a90gkz2mmgdczh1kyn1sc", + "type": "tarball", + "url": "https://github.com/hakimel/reveal.js/archive/3da09f1fef1fa3c140d2daa5bdee2a32683dd964.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "yoda": { + "branch": "0.1.3.0", + "description": "A simple combinator library", + "homepage": null, + "owner": "zenzike", + "repo": "yoda", + "rev": "fb3517e6de9187ece52d3fd0485d5564ac2b0ae8", + "sha256": "115qjyi77nq52k2d9777wwbwilcaiq7y79yv08mdnikk9mrhb212", + "type": "tarball", + "url": "https://github.com/zenzike/yoda/archive/fb3517e6de9187ece52d3fd0485d5564ac2b0ae8.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/sources.nix b/nix/sources.nix similarity index 100% rename from sources.nix rename to nix/sources.nix diff --git a/shell.nix b/shell.nix index 278cf46..bc67366 100644 --- a/shell.nix +++ b/shell.nix @@ -1,4 +1,4 @@ -{ nixpkgs ? import ./nixpkgs.nix {} +{ nixpkgs ? import nix/nixpkgs.nix {} }: with nixpkgs; diff --git a/sources.json b/sources.json deleted file mode 100644 index 322e8fc..0000000 --- a/sources.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "clash-compiler": { - "branch": "1.4", - "description": "Haskell to VHDL/Verilog/SystemVerilog compiler", - "homepage": "https://www.clash-lang.org/", - "owner": "clash-lang", - "repo": "clash-compiler", - "rev": "fa01fd98799cd7c00cae44a9df847142410f1618", - "sha256": "0b42gschkb7sk958a7j529lcw9kmzikhcqjxblm43lggwwy24hns", - "type": "tarball", - "url": "https://github.com/clash-lang/clash-compiler/archive/fa01fd98799cd7c00cae44a9df847142410f1618.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "clash-ffi-sources": { - "branch": "master", - "description": "Haskell to VHDL/Verilog/SystemVerilog compiler", - "homepage": "https://clash-lang.org/", - "owner": "clash-lang", - "repo": "clash-compiler", - "rev": "47918754513298acd06ed4f85eef459e291bd644", - "sha256": "0q89gk7a7y856pv6c0w1lgn18n3i35rgy3caxn45g2z0w4lg7c8n", - "type": "tarball", - "url": "https://github.com/clash-lang/clash-compiler/archive/47918754513298acd06ed4f85eef459e291bd644.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "dense": { - "branch": "master", - "description": "dense Haskell library", - "homepage": "", - "owner": "cchalmers", - "repo": "dense", - "rev": "a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa", - "sha256": "1rg7s1ybdac6139j8l511x2f9jg5793wr20vph54j9p6ybybigsd", - "type": "tarball", - "url": "https://github.com/cchalmers/dense/archive/a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "derive-storable": { - "branch": "master", - "description": "Deriving Storable instances using GHC.Generics", - "homepage": "https://hackage.haskell.org/package/derive-storable", - "owner": "mkloczko", - "repo": "derive-storable", - "rev": "b378aa4cec9ce2fc1cebdb9fb617f6a21872d99e", - "sha256": "0bzl0v47y42n9awfp7nh9kvdz049shdr4aldqrjx40b5izwsll91", - "type": "tarball", - "url": "https://github.com/mkloczko/derive-storable/archive/b378aa4cec9ce2fc1cebdb9fb617f6a21872d99e.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "derive-storable-plugin": { - "branch": "master", - "description": null, - "homepage": null, - "owner": "mkloczko", - "repo": "derive-storable-plugin", - "rev": "6082e149e2b8f575394f7e1972d4d2c1d5b0ddb6", - "sha256": "1334w8vcvxg1929b9v1yy5x52in28mr8y05z3vmfz3z7drxm9c83", - "type": "tarball", - "url": "https://github.com/mkloczko/derive-storable-plugin/archive/6082e149e2b8f575394f7e1972d4d2c1d5b0ddb6.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "ghc-typelits-natnormalise": { - "branch": "master", - "description": "Normalise GHC.TypeLits.Nat equations", - "homepage": "", - "owner": "clash-lang", - "repo": "ghc-typelits-natnormalise", - "rev": "def05130ae7b5fc64772755ad7da1320265d5672", - "sha256": "12bnzi1lkv0dg5gvrymwksn6zdv72zb20p0sy2wpk0j99sygic01", - "type": "tarball", - "url": "https://github.com/clash-lang/ghc-typelits-natnormalise/archive/def05130ae7b5fc64772755ad7da1320265d5672.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "mach-nix": { - "branch": "master", - "description": "Create highly reproducible python environments", - "homepage": "", - "owner": "DavHau", - "repo": "mach-nix", - "rev": "8d903072c7b5426d90bc42a008242c76590af916", - "sha256": "1xmz1rzip6cwk7zhrakigl7zg04mrmsvlarcvhwk38zz0x7kbi10", - "type": "tarball", - "url": "https://github.com/DavHau/mach-nix/archive/8d903072c7b5426d90bc42a008242c76590af916.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "maturin-nix": { - "branch": "buildRustPackage", - "description": "Build pyo3 rust wheels in nix", - "homepage": "", - "owner": "cchalmers", - "repo": "maturin-nix", - "rev": "b4e6ad8d045dc1fb6bfc57e3da5e064c8d9d30ca", - "sha256": "1zgz0ccwfz9p925drp00a8469fy0dg7yv2swf7snb3g21j15cs5c", - "type": "tarball", - "url": "https://github.com/cchalmers/maturin-nix/archive/b4e6ad8d045dc1fb6bfc57e3da5e064c8d9d30ca.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "niv": { - "branch": "master", - "description": "Easy dependency management for Nix projects", - "homepage": "https://github.com/nmattia/niv", - "owner": "nmattia", - "repo": "niv", - "rev": "ba57d5a29b4e0f2085917010380ef3ddc3cf380f", - "sha256": "1kpsvc53x821cmjg1khvp1nz7906gczq8mp83664cr15h94sh8i4", - "type": "tarball", - "url": "https://github.com/nmattia/niv/archive/ba57d5a29b4e0f2085917010380ef3ddc3cf380f.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs": { - "branch": "nixos-20.09", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "896270d629efd47d14972e96f4fbb79fc9f45c80", - "sha256": "0xmjjayg19wm6cn88sh724mrsdj6mgrql6r3zc0g4x9bx4y342p7", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/896270d629efd47d14972e96f4fbb79fc9f45c80.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs-2205": { - "branch": "release-22.05", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "284079785291494969f790d01a4296b3d8b63741", - "sha256": "1a7is9ism6qhqbrwmwmp1dhrqhxlmzb83df04m8z27lhqd6al356", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/284079785291494969f790d01a4296b3d8b63741.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs-2305": { - "branch": "release-23.05", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "dd5ea4aa63eafe67a8f394856095f867a001a5ca", - "sha256": "0qzr64bgq326zscnimfcm99fp43n31z3rj7s09z8g03v5v5g02yi", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/dd5ea4aa63eafe67a8f394856095f867a001a5ca.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "stylish-haskell": { - "branch": "main", - "description": "Haskell code prettifier", - "homepage": "", - "owner": "jaspervdj", - "repo": "stylish-haskell", - "rev": "4ec5c509290d8c2b94045189a122d3fca5e45a4e", - "sha256": "0xkrah3cjg6h87vzh16nanj8spgjvz9js5jwf99lwfmwp35wp92r", - "type": "tarball", - "url": "https://github.com/jaspervdj/stylish-haskell/archive/4ec5c509290d8c2b94045189a122d3fca5e45a4e.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "dense": { - "branch": "master", - "description": "dense Haskell library", - "homepage": "", - "owner": "cchalmers", - "repo": "dense", - "rev": "a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa", - "sha256": "1rg7s1ybdac6139j8l511x2f9jg5793wr20vph54j9p6ybybigsd", - "type": "tarball", - "url": "https://github.com/cchalmers/dense/archive/a84e7e43c7efca0ddfe1a5f60ee18cf006dc04fa.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} \ No newline at end of file From 0ff09fdf8082d59000a5ca5b560a2251359c0782 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:19:37 +0100 Subject: [PATCH 03/90] Add a way to not use cabal.project --- cabal-nix.project | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 cabal-nix.project diff --git a/cabal-nix.project b/cabal-nix.project new file mode 100644 index 0000000..0fe1351 --- /dev/null +++ b/cabal-nix.project @@ -0,0 +1,5 @@ +-- To use this file, use `cabal --project-file=cabal-nix.project` + +packages: . + +-- Nix should be providing dense, so no need for it here From 5a4783ca34d3fa138f31806f90815de5663f0d3c Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:34:27 +0100 Subject: [PATCH 04/90] Add instructions for cabal2nix --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 27dfc78..d09669e 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ # rowan-ndarray + +`ndarray.nix` should be updated with + +```sh +$ cabal2nix . > ndarray.nix +``` + +whenever the Cabal file is updated with e.g. new dependencies. From 3fd17d74cf30189e126ca9d9e1fba6480a6b6664 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:34:40 +0100 Subject: [PATCH 05/90] Clean up shell.nix --- shell.nix | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/shell.nix b/shell.nix index bc67366..c2e4e52 100644 --- a/shell.nix +++ b/shell.nix @@ -1,18 +1,7 @@ -{ nixpkgs ? import nix/nixpkgs.nix {} -}: - -with nixpkgs; - -let ghc = haskellPackages.ghcWithPackages (pkgs: with pkgs; [ - clash-ghc - clash-prelude - QuickCheck - ghcid - repa - dense - ]); -in -mkShell { - name = "clash-exercises"; - buildInputs = [ ghc ]; -} +{ nixpkgs ? import nix/nixpkgs.nix {} }: +(import ./default.nix { inherit nixpkgs; }).env.overrideAttrs (finalAttrs: prevAttrs: { + buildInputs = with nixpkgs.haskellPackages; prevAttrs.buildInputs ++ [ + haskell-language-server + ghcid.bin + ]; +}) From 0beb8723ebab48cbdf4b8a346cd8360786eb436b Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:41:01 +0100 Subject: [PATCH 06/90] Add build instructions --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index d09669e..85a31eb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,24 @@ # rowan-ndarray +## Development + +### Using Cabal + +This builds like any Cabal project with `cabal build`, `cabal repl`, etc. + +### Using Nix (and Cabal) + +There is a `default.nix` so the project can be built with `nix-build`, and a +`shell.nix` for a development shell with `nix-shell`. + +#### Niv + +Dependencies are maintained using `niv` in `nix/sources.json`. +Source repositories specified in `cabal.project` should be kept up to date with +`nix/sources.json`. + +#### cabal2nix + `ndarray.nix` should be updated with ```sh @@ -7,3 +26,10 @@ $ cabal2nix . > ndarray.nix ``` whenever the Cabal file is updated with e.g. new dependencies. + +#### Nix shell + +Within a Nix shell, ideally you build the project with +`cabal build --project-file=cabal-nix.project` to avoid fetching and building +dependencies specifid in `cabal.project` which are only intended for the +non-Nix build. From bb795bf6a9ae8a38453cfe64487194972c106cec Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 12 Jul 2023 12:46:52 +0100 Subject: [PATCH 07/90] Remove unneeded things from nixpkgs overlay --- nix/nixpkgs.nix | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index d6a9a78..1e517cd 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -1,20 +1,18 @@ { sources ? import ./sources.nix }: -let clash-compiler = import sources.clash-compiler {}; - - overlay = _: pkgs: { - niv = (import sources.niv {}).niv; - haskellPackages = pkgs.haskellPackages.override { - overrides = self: super: { - dense = pkgs.haskell.lib.markUnbroken (pkgs.haskell.lib.dontCheck (pkgs.haskell.lib.overrideSrc super.dense { - src = sources.dense; - })); - yoda = - pkgs.haskell.lib.doJailbreak - (self.callCabal2nix "yoda" sources.yoda {}); - } // clash-compiler; - }; - }; +let overlay = _: pkgs: { + niv = (import sources.niv {}).niv; + haskellPackages = pkgs.haskellPackages.override { + overrides = self: super: { + dense = pkgs.haskell.lib.markUnbroken ( + pkgs.haskell.lib.dontCheck ( # doctests are broken + pkgs.haskell.lib.overrideSrc super.dense { + src = sources.dense; + } + ) + ); + }; + }; +}; in -import sources.nixpkgs - { overlays = [ overlay ] ; config = { }; } +import sources.nixpkgs { overlays = [ overlay ] ; config = {}; } From fe4f16836e3efbac39042e009e343f61c589e9cd Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 12 Jul 2023 13:18:32 +0100 Subject: [PATCH 08/90] copied across updated set up --- TypeableTest.hs | 2 -- ndarray.cabal | 3 ++- src/DType.hs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Numskull.hs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/DType.hs create mode 100644 src/Numskull.hs diff --git a/TypeableTest.hs b/TypeableTest.hs index 8899c54..0a8e1d2 100644 --- a/TypeableTest.hs +++ b/TypeableTest.hs @@ -42,8 +42,6 @@ newtype NDArray a = NDArray (R.Array U s t) --t = NDArray (fromListUnboxed (Z :. (3::Int) :. (3::Int)) [1..9::Int]) ---dynTypeRep (toDyn 4) == dynTypeRep (toDyn 2) ---DArray a + NDArray b = diff --git a/ndarray.cabal b/ndarray.cabal index e9c440b..108eb76 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -13,11 +13,12 @@ extra-doc-files: CHANGELOG.md -- extra-source-files: library - exposed-modules: MyLib + exposed-modules: Numskull -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 , dense + , vector hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall diff --git a/src/DType.hs b/src/DType.hs new file mode 100644 index 0000000..540b1f5 --- /dev/null +++ b/src/DType.hs @@ -0,0 +1,57 @@ +module DType where + +-- Basis for all pointwise operations +class (Show a, Typeable a) => DType a where + -- Numeric + add :: a -> a -> a + subtract :: a -> a -> a + multiply :: a -> a -> a + divide :: a -> a -> Double + div :: a -> a -> a + power :: a -> Double -> Double + pow :: a -> a -> a + log :: a -> a -> a + mod :: a -> a -> Integer + abs :: a -> a + ceil :: a -> a + floor :: a -> a + -- Trig + sin :: a -> a + cos :: a -> a + tan :: a -> a + -- Logical + invert :: a -> a + shiftleft :: a -> a + shiftright :: a -> a + -- Comparative + eq :: a -> a -> Bool + leq :: a -> a -> Bool + geq :: a -> a -> Bool + less :: a -> a -> Bool + greater :: a -> a -> Bool + -- Standard Conversions + dtypeToInt :: a -> Int + dtypeToFloat :: a -> Float + dtypeToDouble :: a -> Double + dtypeToBool :: a -> Bool + + +instance DType Int where + add x y = x + y + multiply x y = x * y + divide x y = + eq x y = x == y + +--instance DType Integer where? + +--instance DType Float where? + +--instance DType Double + +--instance DType Bool where + +--instance DType Char where? + +--instance ByteString where? + +--instance DType BFloat16 where? \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs new file mode 100644 index 0000000..aa64581 --- /dev/null +++ b/src/Numskull.hs @@ -0,0 +1,57 @@ +-- trust me bro ;) +-- :set -fdefer-type-errors + +{-# LANGUAGE GADTs #-} + +module Numskull where + +import Prelude as P +import Data.Maybe (isJust, fromJust) +import Data.Dynamic +import Type.Reflection +import Data.Vector as V + +ty x = typeOf x + +-- NdArray -- +data NdArray where + NdArray :: (Typeable a, DType a) => Vector a -> NdArray + +instance Show NdArray where + show (NdArray x) = show x + +-- To do: https://hackage.haskell.org/package/base-4.18.0.0/docs/GHC-Num.html +instance Num NdArray where + (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of + (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match + (_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first + otherwise -> error ("Cannot convert second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") + where + xtype = ty x + ytype = ty y + + --(NdArray x) - (NDArray y) = + + --(NdArray x) * (NDArray y) + + +-- Helper +eqDType x y = case eqTypeRep (ty x) (ty y) of + Just HRefl -> True + otherwise -> False + +matchDType :: NdArray -> NdArray -> Maybe NdArray +matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of + Just HRefl -> Just $ NdArray (V.map dtypeToInt y) + otherwise -> Nothing + + +---- Testing + +unwrapND :: NdArray -> (String, Vector Dynamic) +unwrapND (NdArray x) = case typeOf x of + vecTypeInt -> ("Int", V.map toDyn x) + vecTypeBool -> ("Bool", V.map toDyn x) + +nd1 = NdArray (fromList [1,2,3::Int]) +nd2 = NdArray (fromList [10,11,12::Int]) \ No newline at end of file From c17388e03bcea024373081b49bda1430b3adf91d Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 12 Jul 2023 13:36:28 +0100 Subject: [PATCH 09/90] first build --- src/DType.hs | 3 ++- src/Numskull.hs | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 540b1f5..037b5a0 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -1,5 +1,7 @@ module DType where +import Type.Reflection + -- Basis for all pointwise operations class (Show a, Typeable a) => DType a where -- Numeric @@ -39,7 +41,6 @@ class (Show a, Typeable a) => DType a where instance DType Int where add x y = x + y multiply x y = x * y - divide x y = eq x y = x == y --instance DType Integer where? diff --git a/src/Numskull.hs b/src/Numskull.hs index aa64581..1f3a943 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -6,12 +6,15 @@ module Numskull where import Prelude as P -import Data.Maybe (isJust, fromJust) +--import Data.Maybe (isJust, fromJust) import Data.Dynamic import Type.Reflection import Data.Vector as V -ty x = typeOf x +import DType + +ty :: Typeable a => a -> TypeRep a +ty x = typeOf x -- Shorthand -- NdArray -- data NdArray where @@ -23,12 +26,11 @@ instance Show NdArray where -- To do: https://hackage.haskell.org/package/base-4.18.0.0/docs/GHC-Num.html instance Num NdArray where (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of - (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - (_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first - otherwise -> error ("Cannot convert second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") + (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match + (_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first + _ -> error ("Cannot convert second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") where - xtype = ty x - ytype = ty y + xtype = ty x; ytype = ty y --(NdArray x) - (NDArray y) = @@ -36,22 +38,25 @@ instance Num NdArray where -- Helper +eqDType :: (Typeable a, Typeable b) => a -> b -> Bool eqDType x y = case eqTypeRep (ty x) (ty y) of - Just HRefl -> True - otherwise -> False + Just HRefl -> True + _ -> False matchDType :: NdArray -> NdArray -> Maybe NdArray matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of - Just HRefl -> Just $ NdArray (V.map dtypeToInt y) - otherwise -> Nothing + Just HRefl -> Just $ NdArray (V.map dtypeToInt y) + _ -> Nothing ---- Testing +{- unwrapND :: NdArray -> (String, Vector Dynamic) unwrapND (NdArray x) = case typeOf x of vecTypeInt -> ("Int", V.map toDyn x) vecTypeBool -> ("Bool", V.map toDyn x) +-} -nd1 = NdArray (fromList [1,2,3::Int]) -nd2 = NdArray (fromList [10,11,12::Int]) \ No newline at end of file +nd1 :: NdArray; nd2 :: NdArray +nd1 = NdArray (fromList [1,2,3::Int]); nd2 = NdArray (fromList [10,11,12::Int]) \ No newline at end of file From 36da8c3803637205d99ae372d8376b4adf35d6dc Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 12 Jul 2023 16:22:43 +0100 Subject: [PATCH 10/90] implemented dtypes --- src/DType.hs | 143 ++++++++++++++++++++++++++++++++++++++++++++---- src/Numskull.hs | 5 +- 2 files changed, 136 insertions(+), 12 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 037b5a0..85d15e5 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -1,6 +1,8 @@ module DType where +import Prelude as P import Type.Reflection +import GHC.Float (float2Double) -- Basis for all pointwise operations class (Show a, Typeable a) => DType a where @@ -33,23 +35,144 @@ class (Show a, Typeable a) => DType a where greater :: a -> a -> Bool -- Standard Conversions dtypeToInt :: a -> Int - dtypeToFloat :: a -> Float - dtypeToDouble :: a -> Double - dtypeToBool :: a -> Bool + --dtypeToFloat :: a -> Float + --dtypeToDouble :: a -> Double + --dtypeToBool :: a -> Bool +{- +--INSTANCE TEMPLATE-- +instance DType TYPE where + -- Numeric + add x y = + subtract x y = + multiply x y = + divide x y = double + div x y = + power x d = double + pow x y = + log x y = + mod x y = integer + abs x = + ceil x = + floor x = + -- Trig + sin x = + cos x = + tan x = + -- Logical + invert x = + shiftleft x = + shiftright x = + -- Comparative + eq x y = + leq x y = + geq x y = + less x y = + greater x y = + -- Standard Conversions + dtypeToInt x = + dtypeToFloat x = + dtypeToDouble x = + dtypeToBool x = +-} + +instance DType Int where + -- Numeric + add x y = x + y + subtract x y = x - y + multiply x y = x * y + divide x y = fromIntegral x / fromIntegral y + div = P.div + power x d = fromIntegral x ** d + pow x y = x ^ y + log x y = P.floor $ logBase (fromIntegral x) (fromIntegral y) :: Int + mod x y = fromIntegral (x `P.mod` y) :: Integer + abs = P.abs + ceil x = x + floor x = x + -- Trig + sin x = (round $ P.sin $ fromIntegral x) :: Int + cos x = (round $ P.cos $ fromIntegral x) :: Int + tan x = (round $ P.tan $ fromIntegral x) :: Int + -- Logical + invert x = -x + shiftleft x = x * 2 + shiftright x = x `P.div` 2 + -- Comparative + eq x y = x == y + leq x y = x <= y + geq x y = x >= y + less x y = x < y + greater x y = x > y + -- (Conversions) + dtypeToInt x = x -instance DType Int where - add x y = x + y - multiply x y = x * y - eq x y = x == y ---instance DType Integer where? +--instance DType Int64 where? ---instance DType Float where? +instance DType Float where + -- Numeric + add x y = x + y + subtract x y = x - y + multiply x y = x * y + divide x y = float2Double (x / y) + div x y = fromIntegral (P.floor x `P.div` P.floor y) :: Float + power x d = float2Double x ** d + pow x y = x ** y + log x y = logBase x y + mod x y = fromIntegral (P.floor x `P.mod` P.floor y) :: Integer + abs = P.abs + ceil x = (fromIntegral $ P.ceiling x) :: Float + floor x = (fromIntegral $ P.floor x) :: Float + -- Trig + sin = P.sin + cos = P.cos + tan = P.tan + -- Logical + invert x = -x + shiftleft x = x * 2 + shiftright x = x / 2 + -- Comparative + eq x y = x == y + leq x y = x <= y + geq x y = x >= y + less x y = x < y + greater x y = x > y + -- (Conversions) + dtypeToInt x = (P.floor x) :: Int --instance DType Double ---instance DType Bool where +instance DType Bool where + -- Numeric + add x y = x || y + subtract x y = toEnum (fromEnum x - fromEnum y) + multiply x y = x && y + divide _x _y = undefined + div x y = (x || y) && not (x && y) + power _x _d = undefined + pow x y = toEnum (fromEnum x ^ fromEnum y) + log _x _y = undefined + mod x y = fromIntegral (fromEnum x `P.mod` fromEnum y) :: Integer + abs _ = True + ceil x = x + floor x = x + -- Trig + sin x = toEnum (round $ P.sin $ fromIntegral $ fromEnum x) :: Bool + cos x = toEnum (round $ P.cos $ fromIntegral $ fromEnum x) :: Bool + tan x = toEnum (round $ P.tan $ fromIntegral $ fromEnum x) :: Bool + -- Logical + invert x = not x + shiftleft _ = False + shiftright _ = False + -- Comparative + eq x y = x == y + leq x y = x <= y + geq x y = x >= y + less x y = x < y + greater x y = x > y + -- (Conversions) + dtypeToInt x = (fromEnum x) :: Int --instance DType Char where? diff --git a/src/Numskull.hs b/src/Numskull.hs index 1f3a943..be5beb4 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -27,8 +27,9 @@ instance Show NdArray where instance Num NdArray where (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - (_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first - _ -> error ("Cannot convert second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") + -- Code to auto-cast types + --(_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first + _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") where xtype = ty x; ytype = ty y From c7728735dcbb0b41fb79edc5a108060da1320486 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 12 Jul 2023 17:29:02 +0100 Subject: [PATCH 11/90] pointwise generic in progress --- src/DType.hs | 25 ++++++++++++++++--------- src/Numskull.hs | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 85d15e5..4846277 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TypeApplications #-} + module DType where import Prelude as P @@ -85,15 +87,15 @@ instance DType Int where div = P.div power x d = fromIntegral x ** d pow x y = x ^ y - log x y = P.floor $ logBase (fromIntegral x) (fromIntegral y) :: Int + log x y = (P.floor (logBase ((fromIntegral x) :: Double) ((fromIntegral y) :: Double))) :: Int mod x y = fromIntegral (x `P.mod` y) :: Integer abs = P.abs ceil x = x floor x = x -- Trig - sin x = (round $ P.sin $ fromIntegral x) :: Int - cos x = (round $ P.cos $ fromIntegral x) :: Int - tan x = (round $ P.tan $ fromIntegral x) :: Int + sin x = (round $ P.sin $ iTf x) :: Int + cos x = (round $ P.cos $ iTf x) :: Int + tan x = (round $ P.tan $ iTf x) :: Int -- Logical invert x = -x shiftleft x = x * 2 @@ -107,6 +109,8 @@ instance DType Int where -- (Conversions) dtypeToInt x = x +iTf = fromIntegral @Int @Float + --instance DType Int64 where? @@ -116,11 +120,11 @@ instance DType Float where subtract x y = x - y multiply x y = x * y divide x y = float2Double (x / y) - div x y = fromIntegral (P.floor x `P.div` P.floor y) :: Float + div x y = (fromIntegral (P.floor x `P.div` P.floor y)) :: Float power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y - mod x y = fromIntegral (P.floor x `P.mod` P.floor y) :: Integer + mod x y = (fromIntegral (P.floor x `P.mod` P.floor y)) :: Integer abs = P.abs ceil x = (fromIntegral $ P.ceiling x) :: Float floor x = (fromIntegral $ P.floor x) :: Float @@ -158,9 +162,9 @@ instance DType Bool where ceil x = x floor x = x -- Trig - sin x = toEnum (round $ P.sin $ fromIntegral $ fromEnum x) :: Bool - cos x = toEnum (round $ P.cos $ fromIntegral $ fromEnum x) :: Bool - tan x = toEnum (round $ P.tan $ fromIntegral $ fromEnum x) :: Bool + sin = boolEnumOp P.sin + cos = boolEnumOp P.cos + tan = boolEnumOp P.tan -- Logical invert x = not x shiftleft _ = False @@ -174,6 +178,9 @@ instance DType Bool where -- (Conversions) dtypeToInt x = (fromEnum x) :: Int +boolEnumOp :: (RealFrac a1 , Num a2) => (a2 -> a1) -> Bool -> Bool +boolEnumOp f x = 0 /= ((round $ f $ fromIntegral $ fromEnum x) :: Integer) + --instance DType Char where? --instance ByteString where? diff --git a/src/Numskull.hs b/src/Numskull.hs index be5beb4..1e7955d 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -3,10 +3,10 @@ {-# LANGUAGE GADTs #-} + module Numskull where import Prelude as P ---import Data.Maybe (isJust, fromJust) import Data.Dynamic import Type.Reflection import Data.Vector as V @@ -23,19 +23,16 @@ data NdArray where instance Show NdArray where show (NdArray x) = show x --- To do: https://hackage.haskell.org/package/base-4.18.0.0/docs/GHC-Num.html instance Num NdArray where (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - -- Code to auto-cast types - --(_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") where xtype = ty x; ytype = ty y - --(NdArray x) - (NDArray y) = - + --x - y = pointwiseZip DType.subtract x y --(NdArray x) * (NDArray y) + -- To do: https://hackage.haskell.org/package/base-4.18.0.0/docs/GHC-Num.html -- Helper @@ -49,6 +46,35 @@ matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int Just HRefl -> Just $ NdArray (V.map dtypeToInt y) _ -> Nothing +-- Pointwise Operations not defined in Num + +{- Pointwise zip with type conversion (TODO) +pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of + (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match + -- Code to auto-cast types + --(_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first + _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") + where + xtype = ty x; ytype = ty y +-} +pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep xtype ytype of + Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match + Nothing -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") + where + xtype = ty x; ytype = ty y + +elemMultiply x y = pointwiseZip multiply x y + + + + + + + + + + + ---- Testing From cf59e8f0bb347531610750c725f9d6ae2e90c005 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 13 Jul 2023 13:03:08 +0100 Subject: [PATCH 12/90] testing serialisation --- src/Numskull.hs | 2 +- src/Serialisation.hs | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/Serialisation.hs diff --git a/src/Numskull.hs b/src/Numskull.hs index 1e7955d..52fec5f 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -21,7 +21,7 @@ data NdArray where NdArray :: (Typeable a, DType a) => Vector a -> NdArray instance Show NdArray where - show (NdArray x) = show x + show (NdArray x) = show x instance Num NdArray where (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of diff --git a/src/Serialisation.hs b/src/Serialisation.hs new file mode 100644 index 0000000..d130495 --- /dev/null +++ b/src/Serialisation.hs @@ -0,0 +1,48 @@ +module Serialisation where + +import Numskull +import Data.Vector.Storable (unsafeWith) + +-- built in numpy serialisation descriptions +getNumpyDType :: NdArray -> String +getNumpyDType _ = " String +getNumpyShape _ "(3,)" --3x1 explicitly for now + + +-- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 +-- see https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html +saveNpy :: FilePath -> NdArray -> IO () +saveNpy path nd = withBinaryFile path WriteMode $ \h -> do + let + -- Unpack vector + (NdArray v) = nd + -- Header string without length + header = + "{'descr': '"++ getNumpyDType nd ++ "', " ++ + "'fortran_order': False, " ++ + "'shape': "++ getNumpyShape nd ++ " }" + -- Calculate header length (& padding) + unpaddedLen = 6 + 2 + 2 + length header + 1 + paddedLen = ((unpaddedLen + 63) `div` 64) * 64 + padding = paddedLen - unpaddedLen + headerLen = length header + padding + 1 + -- Put header & padding + hPutStr h "\x93NUMPY\x01\x00" + alloca $ \ptr -> poke ptr (fromIntegral headerLen :: Word16) >> hPutBuf h ptr 2 + hPutStr h header + hPutStr h (List.replicate padding ' ') + hPutChar h '\n' + -- Put vector body + --unsafeWithPtr a $ \ptr -> hPutBuf h ptr (size nd * sizeOf (undefined :: (NumpyType a)=>a)) + unsafeWith v (\ptr -> hPutBuf h ptr (size v * sizeOf (undefined :: Int))) + +saveNpz = undefined + +loadNpy = undefined + +loadNpz = undefined + +testsave = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3])) \ No newline at end of file From b5b2f63037f261127e0ec62d495ec009104f534a Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 13 Jul 2023 14:47:47 +0100 Subject: [PATCH 13/90] still trying to get serialisation to work --- src/Numskull.hs | 8 +++++--- src/Serialisation.hs | 26 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 52fec5f..2f2cee9 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -9,7 +9,8 @@ module Numskull where import Prelude as P import Data.Dynamic import Type.Reflection -import Data.Vector as V +---import Data.Vector as V +import Data.Vector.Storable as V import DType @@ -18,7 +19,7 @@ ty x = typeOf x -- Shorthand -- NdArray -- data NdArray where - NdArray :: (Typeable a, DType a) => Vector a -> NdArray + NdArray :: (Typeable a, DType a, Storable a) => Vector a -> NdArray instance Show NdArray where show (NdArray x) = show x @@ -57,6 +58,7 @@ pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType ( where xtype = ty x; ytype = ty y -} +{- pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep xtype ytype of Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match Nothing -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") @@ -65,7 +67,7 @@ pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep xtype ytype of elemMultiply x y = pointwiseZip multiply x y - +-} diff --git a/src/Serialisation.hs b/src/Serialisation.hs index d130495..27601ff 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -1,7 +1,14 @@ module Serialisation where import Numskull -import Data.Vector.Storable (unsafeWith) +import DType + +import System.IO +import Data.Vector.Storable as S +import Data.Word (Word16) +import Data.List as List +import Foreign (ForeignPtr, Ptr, alloca) +import Foreign.Storable (poke, sizeOf) -- built in numpy serialisation descriptions getNumpyDType :: NdArray -> String @@ -9,7 +16,7 @@ getNumpyDType _ = " String -getNumpyShape _ "(3,)" --3x1 explicitly for now +getNumpyShape _ = "(3,)" --3x1 explicitly for now -- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 @@ -21,14 +28,14 @@ saveNpy path nd = withBinaryFile path WriteMode $ \h -> do (NdArray v) = nd -- Header string without length header = - "{'descr': '"++ getNumpyDType nd ++ "', " ++ - "'fortran_order': False, " ++ - "'shape': "++ getNumpyShape nd ++ " }" + "{'descr': '"<> getNumpyDType nd <> "', " <> + "'fortran_order': False, " <> + "'shape': "<> getNumpyShape nd <> " }" -- Calculate header length (& padding) - unpaddedLen = 6 + 2 + 2 + length header + 1 - paddedLen = ((unpaddedLen + 63) `div` 64) * 64 + unpaddedLen = 6 + 2 + 2 + List.length header + 1 + paddedLen = ((unpaddedLen + 63) `Prelude.div` 64) * 64 padding = paddedLen - unpaddedLen - headerLen = length header + padding + 1 + headerLen = List.length header + padding + 1 -- Put header & padding hPutStr h "\x93NUMPY\x01\x00" alloca $ \ptr -> poke ptr (fromIntegral headerLen :: Word16) >> hPutBuf h ptr 2 @@ -45,4 +52,5 @@ loadNpy = undefined loadNpz = undefined -testsave = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3])) \ No newline at end of file +main :: IO () +main = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3])) \ No newline at end of file From e3c45f67604dd9284afc1acd1fb4dc7b670530c6 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 13 Jul 2023 15:23:38 +0100 Subject: [PATCH 14/90] 123! serialisation! --- src/Serialisation.hs | 8 ++++---- src/testout/idk.npy | Bin 0 -> 152 bytes 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 src/testout/idk.npy diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 27601ff..8832145 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -22,10 +22,10 @@ getNumpyShape _ = "(3,)" --3x1 explicitly for now -- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 -- see https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html saveNpy :: FilePath -> NdArray -> IO () -saveNpy path nd = withBinaryFile path WriteMode $ \h -> do +saveNpy path (NdArray v) = withBinaryFile path WriteMode $ \h -> do let -- Unpack vector - (NdArray v) = nd + nd = (NdArray v) -- Header string without length header = "{'descr': '"<> getNumpyDType nd <> "', " <> @@ -44,7 +44,7 @@ saveNpy path nd = withBinaryFile path WriteMode $ \h -> do hPutChar h '\n' -- Put vector body --unsafeWithPtr a $ \ptr -> hPutBuf h ptr (size nd * sizeOf (undefined :: (NumpyType a)=>a)) - unsafeWith v (\ptr -> hPutBuf h ptr (size v * sizeOf (undefined :: Int))) + unsafeWith v (\ptr -> hPutBuf h ptr (3 * sizeOf (undefined :: Int))) saveNpz = undefined @@ -53,4 +53,4 @@ loadNpy = undefined loadNpz = undefined main :: IO () -main = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3])) \ No newline at end of file +main = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3 :: Int])) \ No newline at end of file diff --git a/src/testout/idk.npy b/src/testout/idk.npy new file mode 100644 index 0000000000000000000000000000000000000000..643c53d3b67d826ec05c074c30dfe60ff01457a7 GIT binary patch literal 152 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlWC!@qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= dXCxM+0{I%oI+_Z#3X}jYMg|CAg3`=T8UW*c8(RPX literal 0 HcmV?d00001 From 34835d83ea9cecf9ff251084aae43db0be09a9be Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 13 Jul 2023 16:32:56 +0100 Subject: [PATCH 15/90] basile fixed pointwise! --- src/Numskull.hs | 13 +++++++------ src/Serialisation.hs | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 2f2cee9..2f7cc79 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -2,7 +2,8 @@ -- :set -fdefer-type-errors {-# LANGUAGE GADTs #-} - +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} module Numskull where @@ -42,6 +43,7 @@ eqDType x y = case eqTypeRep (ty x) (ty y) of Just HRefl -> True _ -> False +-- case typeOf x `eqTypeRep` typeRep @Integer of NOTE THIS IS INT SYNTAX matchDType :: NdArray -> NdArray -> Maybe NdArray matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of Just HRefl -> Just $ NdArray (V.map dtypeToInt y) @@ -58,16 +60,15 @@ pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType ( where xtype = ty x; ytype = ty y -} -{- -pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep xtype ytype of + +pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray +pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty y) of Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match Nothing -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") where xtype = ty x; ytype = ty y -elemMultiply x y = pointwiseZip multiply x y - --} +elemMultiply x y = pointwiseZip multiply x y diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 8832145..3659169 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -52,5 +52,5 @@ loadNpy = undefined loadNpz = undefined -main :: IO () -main = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3 :: Int])) \ No newline at end of file +testsave :: IO () +testsave = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3 :: Int])) \ No newline at end of file From 7881d3ef194e020a9ce9aeaf92f29958e69cbe27 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 13 Jul 2023 17:37:30 +0100 Subject: [PATCH 16/90] eq, ord & tidying --- src/DType.hs | 4 ++ src/Numskull.hs | 119 +++++++++++++++++++++++++++++++----------------- 2 files changed, 82 insertions(+), 41 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 4846277..d32ee04 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -19,6 +19,7 @@ class (Show a, Typeable a) => DType a where log :: a -> a -> a mod :: a -> a -> Integer abs :: a -> a + signum :: a -> a ceil :: a -> a floor :: a -> a -- Trig @@ -90,6 +91,7 @@ instance DType Int where log x y = (P.floor (logBase ((fromIntegral x) :: Double) ((fromIntegral y) :: Double))) :: Int mod x y = fromIntegral (x `P.mod` y) :: Integer abs = P.abs + signum = P.signum ceil x = x floor x = x -- Trig @@ -126,6 +128,7 @@ instance DType Float where log x y = logBase x y mod x y = (fromIntegral (P.floor x `P.mod` P.floor y)) :: Integer abs = P.abs + signum = P.signum ceil x = (fromIntegral $ P.ceiling x) :: Float floor x = (fromIntegral $ P.floor x) :: Float -- Trig @@ -159,6 +162,7 @@ instance DType Bool where log _x _y = undefined mod x y = fromIntegral (fromEnum x `P.mod` fromEnum y) :: Integer abs _ = True + signum = x ceil x = x floor x = x -- Trig diff --git a/src/Numskull.hs b/src/Numskull.hs index 2f7cc79..07e8f99 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -12,44 +12,101 @@ import Data.Dynamic import Type.Reflection ---import Data.Vector as V import Data.Vector.Storable as V - import DType +-- Typing shorthand ty :: Typeable a => a -> TypeRep a -ty x = typeOf x -- Shorthand +ty x = typeOf x -- NdArray -- data NdArray where NdArray :: (Typeable a, DType a, Storable a) => Vector a -> NdArray +-- Todo: show in a nicer shapely form :) instance Show NdArray where show (NdArray x) = show x +-- Todo: check shapes are also equal +instance Eq NdArray where + (NdArray x) == (NdArray y) = x == y + (NdArray x) /= (NdArray y) = x /= y + +-- Todo: should we have some notion of LEQ for a smaller size vector? +-- Investigate how Vector implements further +instance Ord NdArray where + compare (NdArray x) (NdArray y) = compare x y + (<) (NdArray x) (NdArray y) = x < y + (<=) (NdArray x) (NdArray y) = x <= y + (>) (NdArray x) (NdArray y) = x > y + (>=) (NdArray x) (NdArray y) = x >= y + max (NdArray x) = max x + min (NdArray x) = min x + +-- To do: matrix multiplication :O instance Num NdArray where - (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of - (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") - where - xtype = ty x; ytype = ty y + (+) = pointwiseZip add + (-) = pointwiseZip subtract + (*) = undefined -- Matrix multiplication + negate = invert + abs = abs + signum = signum + fromInteger x = NdArray (fromList [x]) + +-- Pointwise Functions + -- All the numpy-like functions not defined within the Eq, Ord or Num instances + + -- Single Argument + + -- To do ;) + + -- Two Arguments + pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray + pointwiseZip zipfunc (NdArray x) (NdArray y) = + case eqTypeRep (ty x) (ty y) of + Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match + Nothing -> error ("Cannot match second matrix of type '" P.++ show (ty y) P.++ "' to type '" P.++ show (ty x) P.++ "'.") + + elemMultiply :: NdArray -> NdArray -> NdArray + elemMultiply = pointwiseZip multiply + + elemDivide :: NdArray -> NdArray -> NdArray + elemDivide = pointwiseZip divide + + elemDiv :: NdArray -> NdArray -> NdArray + elemDiv = pointwiseZip div + + elemPower :: NdArray -> NdArray -> NdArray + elemPower = pointwiseZip power + + elemPow :: NdArray -> NdArray -> NdArray + elemPow = pointwiseZip pow + +-- Type & Shape Conversion + -- Converting between the standard dtypes and changing the shapes of matricies + + -- To do: add many more possible types you can convert to + -- Use the TypeApplications syntax: + -- case typeOf x `eqTypeRep` typeRep @Integer of + matchDType :: NdArray -> NdArray -> Maybe NdArray + matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of + Just HRefl -> Just $ NdArray (V.map dtypeToInt y) + _ -> Nothing - --x - y = pointwiseZip DType.subtract x y - --(NdArray x) * (NDArray y) - -- To do: https://hackage.haskell.org/package/base-4.18.0.0/docs/GHC-Num.html --- Helper -eqDType :: (Typeable a, Typeable b) => a -> b -> Bool -eqDType x y = case eqTypeRep (ty x) (ty y) of - Just HRefl -> True - _ -> False --- case typeOf x `eqTypeRep` typeRep @Integer of NOTE THIS IS INT SYNTAX -matchDType :: NdArray -> NdArray -> Maybe NdArray -matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of - Just HRefl -> Just $ NdArray (V.map dtypeToInt y) - _ -> Nothing --- Pointwise Operations not defined in Num + + + + +---- Testing + +-- Helper trying to simplify the type checking.... +eqDType :: (Typeable a, Typeable b) => a -> b -> Bool +eqDType x y = case eqTypeRep (ty x) (ty y) of + Just HRefl -> True + _ -> False {- Pointwise zip with type conversion (TODO) pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of @@ -61,26 +118,6 @@ pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType ( xtype = ty x; ytype = ty y -} -pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray -pointwiseZip zipfunc (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty y) of - Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match - Nothing -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") - where - xtype = ty x; ytype = ty y - -elemMultiply x y = pointwiseZip multiply x y - - - - - - - - - - ----- Testing - {- unwrapND :: NdArray -> (String, Vector Dynamic) unwrapND (NdArray x) = case typeOf x of From 394f26d1f834bd2283972acb8451bd6e2231d539 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 14 Jul 2023 09:05:54 +0100 Subject: [PATCH 17/90] just before I break everything by adding shapes --- src/Numskull.hs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 07e8f99..13313bf 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -1,18 +1,15 @@ --- trust me bro ;) --- :set -fdefer-type-errors - {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-} module Numskull where +import DType + import Prelude as P -import Data.Dynamic -import Type.Reflection ----import Data.Vector as V import Data.Vector.Storable as V -import DType +import Data.Dynamic -- Not needed? +import Type.Reflection -- Typing shorthand ty :: Typeable a => a -> TypeRep a From 167f93e2967a8795331733a1206ecdffa25a7c55 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 14 Jul 2023 15:41:40 +0100 Subject: [PATCH 18/90] added shapes & it compiles, fixed ord, ord => dtyp --- src/DType.hs | 21 +++- src/Numskull.hs | 221 ++++++++++++++++++++++++++++++------------- src/Serialisation.hs | 3 + 3 files changed, 177 insertions(+), 68 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index d32ee04..23df8eb 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -3,11 +3,13 @@ module DType where import Prelude as P +import Data.Vector.Storable import Type.Reflection import GHC.Float (float2Double) -- Basis for all pointwise operations -class (Show a, Typeable a) => DType a where +class (Show a, Typeable a, Storable a, Eq a, Ord a) => DType a where + identity :: a -- Numeric add :: a -> a -> a subtract :: a -> a -> a @@ -30,12 +32,14 @@ class (Show a, Typeable a) => DType a where invert :: a -> a shiftleft :: a -> a shiftright :: a -> a + {- -- Comparative eq :: a -> a -> Bool leq :: a -> a -> Bool geq :: a -> a -> Bool less :: a -> a -> Bool greater :: a -> a -> Bool + -} -- Standard Conversions dtypeToInt :: a -> Int --dtypeToFloat :: a -> Float @@ -80,6 +84,7 @@ instance DType TYPE where -} instance DType Int where + identity = 0 -- Numeric add x y = x + y subtract x y = x - y @@ -103,11 +108,13 @@ instance DType Int where shiftleft x = x * 2 shiftright x = x `P.div` 2 -- Comparative + {- eq x y = x == y leq x y = x <= y geq x y = x >= y less x y = x < y greater x y = x > y + -} -- (Conversions) dtypeToInt x = x @@ -117,6 +124,7 @@ iTf = fromIntegral @Int @Float --instance DType Int64 where? instance DType Float where + identity = 0.0 -- Numeric add x y = x + y subtract x y = x - y @@ -140,17 +148,20 @@ instance DType Float where shiftleft x = x * 2 shiftright x = x / 2 -- Comparative + {- eq x y = x == y leq x y = x <= y geq x y = x >= y less x y = x < y greater x y = x > y + -} -- (Conversions) dtypeToInt x = (P.floor x) :: Int --instance DType Double instance DType Bool where + identity = False -- Numeric add x y = x || y subtract x y = toEnum (fromEnum x - fromEnum y) @@ -162,9 +173,9 @@ instance DType Bool where log _x _y = undefined mod x y = fromIntegral (fromEnum x `P.mod` fromEnum y) :: Integer abs _ = True - signum = x - ceil x = x - floor x = x + signum = id + ceil = id + floor = id -- Trig sin = boolEnumOp P.sin cos = boolEnumOp P.cos @@ -174,11 +185,13 @@ instance DType Bool where shiftleft _ = False shiftright _ = False -- Comparative + {- eq x y = x == y leq x y = x <= y geq x y = x >= y less x y = x < y greater x y = x > y + -} -- (Conversions) dtypeToInt x = (fromEnum x) :: Int diff --git a/src/Numskull.hs b/src/Numskull.hs index 13313bf..13d0e02 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -1,6 +1,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} module Numskull where @@ -15,83 +16,166 @@ import Type.Reflection ty :: Typeable a => a -> TypeRep a ty x = typeOf x +(=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) +(=@=) v u = eqTypeRep (ty v) (ty u) + +-- Todo: Should shapes be [Integer] or [Int] or maybe even another vector? -- NdArray -- +-- The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are +-- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). data NdArray where - NdArray :: (Typeable a, DType a, Storable a) => Vector a -> NdArray + NdArray :: DType a => Vector a -> [Integer] -> NdArray -- Todo: show in a nicer shapely form :) instance Show NdArray where - show (NdArray x) = show x + show (NdArray v s) = show v <> show s --- Todo: check shapes are also equal instance Eq NdArray where - (NdArray x) == (NdArray y) = x == y - (NdArray x) /= (NdArray y) = x /= y + (NdArray v s) == (NdArray u r) = (r == s) && + case v =@= u of + Just HRefl -> v == u + Nothing -> False + (NdArray v s) /= (NdArray u r) = (r /= s) || + case v =@= u of + Just HRefl -> v /= u + Nothing -> True --- Todo: should we have some notion of LEQ for a smaller size vector? -- Investigate how Vector implements further +-- Todo: check max and min work properly on all dtypes, probably use a map instead instance Ord NdArray where - compare (NdArray x) (NdArray y) = compare x y - (<) (NdArray x) (NdArray y) = x < y - (<=) (NdArray x) (NdArray y) = x <= y - (>) (NdArray x) (NdArray y) = x > y - (>=) (NdArray x) (NdArray y) = x >= y - max (NdArray x) = max x - min (NdArray x) = min x + (NdArray v s) `compare` (NdArray u r) = if s == r then case v =@= u of + Just HRefl -> compare v u + Nothing -> error $ typeMismatch (show v) (show u) + else error $ shapeMismatch (show s) (show r) + + (NdArray v s) <= (NdArray u r) = if s == r then case v =@= u of + Just HRefl -> v <= u + Nothing -> error $ typeMismatch (show v) (show u) + else error $ shapeMismatch (show s) (show r) + -- (>) (NdArray v s) (NdArray u r) = u > v + -- (>=) (NdArray v s) (NdArray u r) = u >= v + -- max (NdArray v s) = V.maximum v + -- min (NdArray v s) = V.minimum v -- To do: matrix multiplication :O +-- To do: change fromInteger to return an integer array rather than int instance Num NdArray where (+) = pointwiseZip add - (-) = pointwiseZip subtract + (-) = pointwiseZip DType.subtract (*) = undefined -- Matrix multiplication - negate = invert - abs = abs - signum = signum - fromInteger x = NdArray (fromList [x]) + negate (NdArray v s) = NdArray (V.map DType.invert v) s + abs (NdArray v s) = NdArray (V.map DType.abs v) s + signum (NdArray v s) = NdArray (V.map DType.signum v) s + fromInteger x = NdArray (fromList [(fromInteger x) :: Int]) [1] --- Pointwise Functions - -- All the numpy-like functions not defined within the Eq, Ord or Num instances - - -- Single Argument - - -- To do ;) - - -- Two Arguments - pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray - pointwiseZip zipfunc (NdArray x) (NdArray y) = - case eqTypeRep (ty x) (ty y) of - Just HRefl -> NdArray (V.zipWith zipfunc x y) -- Types match - Nothing -> error ("Cannot match second matrix of type '" P.++ show (ty y) P.++ "' to type '" P.++ show (ty x) P.++ "'.") - - elemMultiply :: NdArray -> NdArray -> NdArray - elemMultiply = pointwiseZip multiply - - elemDivide :: NdArray -> NdArray -> NdArray - elemDivide = pointwiseZip divide - - elemDiv :: NdArray -> NdArray -> NdArray - elemDiv = pointwiseZip div - - elemPower :: NdArray -> NdArray -> NdArray - elemPower = pointwiseZip power - - elemPow :: NdArray -> NdArray -> NdArray - elemPow = pointwiseZip pow - --- Type & Shape Conversion - -- Converting between the standard dtypes and changing the shapes of matricies - -- To do: add many more possible types you can convert to - -- Use the TypeApplications syntax: - -- case typeOf x `eqTypeRep` typeRep @Integer of - matchDType :: NdArray -> NdArray -> Maybe NdArray - matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of - Just HRefl -> Just $ NdArray (V.map dtypeToInt y) - _ -> Nothing +-- Indexing +-- Since vectors are 1D arrays but the matricicies can have n-dimensions, index conversion is neccessary +-- The index i will be the 1D index +-- Then x y z... for each dimension index +-- i = x + y*xsize + z*xsize*ysize + ... +-- x = i % xsize; y = i/(xsize) % ysize; z = i/(xsize*ysize) % zsize; ... +-- As described: https://softwareengineering.stackexchange.com/questions/212808/treating-a-1d-data-structure-as-2d-grid + +-- helper for collapseInd +-- can this be folded? its over two things so i dont think so... only if i zip it +collapseRun :: [Integer] -> [Integer] -> Integer -> Integer +collapseRun _ [] _ = 0 +collapseRun [] _ _ = 0 +collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) + +collapseInd :: [Integer] -> [Integer] -> Integer +collapseInd shape indicies = collapseRun shape indicies 1 + +-- helper for expandInd +expandRun :: [Integer] -> Integer -> Integer -> [Integer] +expandRun [] _ _ = [] +expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) + where x = (i `P.div` runSize) `P.mod` s + +expandInd :: [Integer] -> Integer -> [Integer] +expandInd shape i = expandRun shape i 1 +-- Pointwise Functions +-- All the numpy-like functions not defined within the Eq, Ord or Num instances +-- Single Argument + +-- To do ;) + +-- Two Arguments +-- https://hackage.haskell.org/package/base-4.18.0.0/docs/Control-Monad-ST.html#v:runST +pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray +pointwiseZip zipfunc (NdArray v s) (NdArray u r) = if s == r then + case v =@= u of + Just HRefl -> NdArray (V.zipWith zipfunc v u) s -- Types match + Nothing -> error $ typeMismatch (show$ty v) (show$ty u) + else error $ shapeMismatch (show s) (show r) +elemMultiply :: NdArray -> NdArray -> NdArray +elemMultiply = pointwiseZip multiply +-- Todo: Needs to operate on doubles +--elemDivide :: NdArray -> NdArray -> NdArray +--elemDivide = pointwiseZip divide +elemDiv :: NdArray -> NdArray -> NdArray +elemDiv = pointwiseZip DType.div + +-- Todo: Needs to operate on doubles +--elemPower :: NdArray -> NdArray -> NdArray +--elemPower = pointwiseZip power + +elemPow :: NdArray -> NdArray -> NdArray +elemPow = pointwiseZip pow + +-- Type & Shape Conversion +-- Converting between the standard dtypes and changing the shapes of matricies + +-- To do: add many more possible types you can convert to +-- Use the TypeApplications syntax: +-- case typeOf x `eqTypeRep` typeRep @Integer of +matchDType :: NdArray -> NdArray -> Maybe NdArray +matchDType (NdArray v _) (NdArray u r) = case v =@= fromList [1::Int] of + Just HRefl -> Just $ NdArray (V.map dtypeToInt u) r + _ -> Nothing + +-- Check that the matrix isn't larger than the shape but if so truncate it +constrainSize :: DType a => Vector a -> [Integer] -> (Bool, Vector a) +constrainSize v s = + if size < len then (False, V.take size v) + else (True, v) + where + size = fromInteger (P.product s) :: Int + len = V.length v + +-- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) +padZeros :: DType a => Vector a -> [Integer] -> Vector a +padZeros v s = v V.++ V.replicate (size - len) identity + where + size = fromInteger (P.product s) :: Int + len = V.length v + +-- Contrain or pad the vector to match the size +setSize :: DType a => Vector a -> [Integer] -> Vector a +setSize v s = let (unchanged, u) = constrainSize v s in + if unchanged then padZeros u s else u + +-- Constrain or pad the NdArray to match the new given size +resize :: NdArray -> [Integer] -> NdArray +resize (NdArray v _) r = NdArray (setSize v r) r + +--NB: reshape will pad/truncate individual dimensions whereas resize keeps as many values as possible but they might switch position +-- a matrix being reshaped must already match the size correctly + +padDimension :: NdArray -> [Integer] -> NdArray +padDimension = undefined + +-- Common Errors +shapeMismatch :: String -> String -> String +shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." + +typeMismatch :: String -> String -> String +typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array of type '" <> t2 <> "'." @@ -100,16 +184,23 @@ instance Num NdArray where ---- Testing -- Helper trying to simplify the type checking.... +{- eqDType :: (Typeable a, Typeable b) => a -> b -> Bool -eqDType x y = case eqTypeRep (ty x) (ty y) of - Just HRefl -> True - _ -> False +eqDType x y = case eqTypeRep (typeOf x) (typeOf y) of + Just HRefl -> True + _ -> False + +idk = if eqDType nd1 nd2 then V.zipWith (\x y -> show x <> show y) v1 v2 else fromList [1] + where + (NdArray v1) = nd1 + (NdArray v2) = nd2 +-} {- Pointwise zip with type conversion (TODO) -pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of +pointwiseZip (NdArray v s) (NdArray u r) = case (eqTypeRep xtype ytype, matchDType (NdArray v s) (NdArray u r)) of (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match -- Code to auto-cast types - --(_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first + --(_, Just casted) -> (NdArray v s) + casted -- Second type can be converted to first _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") where xtype = ty x; ytype = ty y @@ -117,10 +208,12 @@ pointwiseZip (NdArray x) (NdArray y) = case (eqTypeRep xtype ytype, matchDType ( {- unwrapND :: NdArray -> (String, Vector Dynamic) -unwrapND (NdArray x) = case typeOf x of +unwrapND (NdArray v s) = case typeOf x of vecTypeInt -> ("Int", V.map toDyn x) vecTypeBool -> ("Bool", V.map toDyn x) -} -nd1 :: NdArray; nd2 :: NdArray -nd1 = NdArray (fromList [1,2,3::Int]); nd2 = NdArray (fromList [10,11,12::Int]) \ No newline at end of file +nd1 :: NdArray +nd2 :: NdArray +nd1 = NdArray (fromList [1,2,3::Int]) [3] +nd2 = NdArray (fromList [10,11,12::Int]) [3] diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 3659169..203af8a 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -14,6 +14,9 @@ import Foreign.Storable (poke, sizeOf) getNumpyDType :: NdArray -> String getNumpyDType _ = " String getNumpyShape _ = "(3,)" --3x1 explicitly for now From 23e86e9713420554c52d65496d2137daf5ec034f Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 14 Jul 2023 17:33:18 +0100 Subject: [PATCH 19/90] working on reshaping --- src/DType.hs | 26 +------------------------- src/Numskull.hs | 39 ++++++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 23df8eb..9607f35 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -8,7 +8,7 @@ import Type.Reflection import GHC.Float (float2Double) -- Basis for all pointwise operations -class (Show a, Typeable a, Storable a, Eq a, Ord a) => DType a where +class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where identity :: a -- Numeric add :: a -> a -> a @@ -32,14 +32,6 @@ class (Show a, Typeable a, Storable a, Eq a, Ord a) => DType a where invert :: a -> a shiftleft :: a -> a shiftright :: a -> a - {- - -- Comparative - eq :: a -> a -> Bool - leq :: a -> a -> Bool - geq :: a -> a -> Bool - less :: a -> a -> Bool - greater :: a -> a -> Bool - -} -- Standard Conversions dtypeToInt :: a -> Int --dtypeToFloat :: a -> Float @@ -107,14 +99,6 @@ instance DType Int where invert x = -x shiftleft x = x * 2 shiftright x = x `P.div` 2 - -- Comparative - {- - eq x y = x == y - leq x y = x <= y - geq x y = x >= y - less x y = x < y - greater x y = x > y - -} -- (Conversions) dtypeToInt x = x @@ -147,14 +131,6 @@ instance DType Float where invert x = -x shiftleft x = x * 2 shiftright x = x / 2 - -- Comparative - {- - eq x y = x == y - leq x y = x <= y - geq x y = x >= y - less x y = x < y - greater x y = x > y - -} -- (Conversions) dtypeToInt x = (P.floor x) :: Int diff --git a/src/Numskull.hs b/src/Numskull.hs index 13d0e02..07c76f9 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -12,7 +12,7 @@ import Data.Vector.Storable as V import Data.Dynamic -- Not needed? import Type.Reflection --- Typing shorthand +-- | Typing shorthand | -- ty :: Typeable a => a -> TypeRep a ty x = typeOf x @@ -69,7 +69,7 @@ instance Num NdArray where fromInteger x = NdArray (fromList [(fromInteger x) :: Int]) [1] --- Indexing +-- | Indexing & Slicing | -- -- Since vectors are 1D arrays but the matricicies can have n-dimensions, index conversion is neccessary -- The index i will be the 1D index -- Then x y z... for each dimension index @@ -96,7 +96,10 @@ expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) expandInd :: [Integer] -> Integer -> [Integer] expandInd shape i = expandRun shape i 1 --- Pointwise Functions +-- The actual indexing bit todo +-- Slicing Todo :) + +-- | Pointwise Functions | -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances -- Single Argument @@ -128,7 +131,7 @@ elemDiv = pointwiseZip DType.div elemPow :: NdArray -> NdArray -> NdArray elemPow = pointwiseZip pow --- Type & Shape Conversion +-- | Type & Shape Conversion | -- -- Converting between the standard dtypes and changing the shapes of matricies -- To do: add many more possible types you can convert to @@ -149,8 +152,8 @@ constrainSize v s = len = V.length v -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) -padZeros :: DType a => Vector a -> [Integer] -> Vector a -padZeros v s = v V.++ V.replicate (size - len) identity +padSize :: DType a => Vector a -> [Integer] -> Vector a +padSize v s = v V.++ V.replicate (size - len) identity where size = fromInteger (P.product s) :: Int len = V.length v @@ -158,7 +161,7 @@ padZeros v s = v V.++ V.replicate (size - len) identity -- Contrain or pad the vector to match the size setSize :: DType a => Vector a -> [Integer] -> Vector a setSize v s = let (unchanged, u) = constrainSize v s in - if unchanged then padZeros u s else u + if unchanged then padSize u s else u -- Constrain or pad the NdArray to match the new given size resize :: NdArray -> [Integer] -> NdArray @@ -167,10 +170,14 @@ resize (NdArray v _) r = NdArray (setSize v r) r --NB: reshape will pad/truncate individual dimensions whereas resize keeps as many values as possible but they might switch position -- a matrix being reshaped must already match the size correctly -padDimension :: NdArray -> [Integer] -> NdArray -padDimension = undefined +map1DIndex s r i = collapseInd r (expandInd s i) + +-- ok then what im gonna do is make an array of all the mapping and value pairs and // it + +padShape :: NdArray -> [Integer] -> NdArray +padDimension (NdArray v s) r = undefined --- Common Errors +-- | Common Errors | -- shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." @@ -184,16 +191,18 @@ typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array ---- Testing -- Helper trying to simplify the type checking.... -{- + +{- I think this is a dead end.... just do the case by case eqDType :: (Typeable a, Typeable b) => a -> b -> Bool eqDType x y = case eqTypeRep (typeOf x) (typeOf y) of Just HRefl -> True _ -> False -idk = if eqDType nd1 nd2 then V.zipWith (\x y -> show x <> show y) v1 v2 else fromList [1] - where - (NdArray v1) = nd1 - (NdArray v2) = nd2 +equey :: NdArray -> NdArray -> Vector Bool +equey (NdArray v1 s1) (NdArray v2 s2) = + if eqDType (NdArray v1 s1) (NdArray v2 s2) + then V.zipWith (==) v1 v2 + else (fromList [True]) :: Vector Bool -} {- Pointwise zip with type conversion (TODO) From 88923ffad89e1f4caad5c2ccc28a76df17b15cb7 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 17 Jul 2023 14:06:49 +0100 Subject: [PATCH 20/90] resizing and padding --- src/Numskull.hs | 53 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 07c76f9..810cc9c 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -66,8 +66,17 @@ instance Num NdArray where negate (NdArray v s) = NdArray (V.map DType.invert v) s abs (NdArray v s) = NdArray (V.map DType.abs v) s signum (NdArray v s) = NdArray (V.map DType.signum v) s - fromInteger x = NdArray (fromList [(fromInteger x) :: Int]) [1] + fromInteger x = NdArray (singleton $ fromInteger @Int x) [1] +-- | Creation | -- +-- Todo: get the ident of the dtype from an nd array +indentityElem = undefined + +indentityElem' :: forall a . DType a => Vector a -> a +indentityElem' _ = DType.identity :: DType a => a + +-- Todo: Create ident array of certain shape +zeros = undefined -- | Indexing & Slicing | -- -- Since vectors are 1D arrays but the matricicies can have n-dimensions, index conversion is neccessary @@ -130,7 +139,7 @@ elemDiv = pointwiseZip DType.div elemPow :: NdArray -> NdArray -> NdArray elemPow = pointwiseZip pow - + -- | Type & Shape Conversion | -- -- Converting between the standard dtypes and changing the shapes of matricies @@ -167,16 +176,46 @@ setSize v s = let (unchanged, u) = constrainSize v s in resize :: NdArray -> [Integer] -> NdArray resize (NdArray v _) r = NdArray (setSize v r) r +reshape :: NdArray -> [Integer] -> Maybe NdArray +reshape (NdArray v s) r = if P.product s == P.product r + then Just $ NdArray v r + else Nothing + --NB: reshape will pad/truncate individual dimensions whereas resize keeps as many values as possible but they might switch position -- a matrix being reshaped must already match the size correctly - +map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer map1DIndex s r i = collapseInd r (expandInd s i) -- ok then what im gonna do is make an array of all the mapping and value pairs and // it +mapShapeLoss :: [Integer] -> [Integer] -> Bool +mapShapeLoss s r = + if P.length r < P.length s then True + else P.or $ P.zipWith (>) s r + +{- +hack :: forall a . DType a => a -> a +hack _ = (DType.identity :: DType a => a) + +vectorHack :: forall a . DType a => Vector a -> Vector a +vectorHack _ = singleton (DType.identity :: DType a => a) + +vectorHack' :: forall a . DType a => Vector a -> a +vectorHack' _ = DType.identity :: DType a => a +-} + +-- If you try to map to a smaller shape, values are dropped & weird stuff happens, otherwise padded padShape :: NdArray -> [Integer] -> NdArray -padDimension (NdArray v s) r = undefined - +padShape (NdArray v s) r = + let + newSize = fromInteger @Int (P.product r) + nullVec = V.replicate newSize (indentityElem' v) + fi i = fromIntegral @Int @Integer i + newIndicies = imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v + in + NdArray (unsafeUpdate_ nullVec newIndicies v) r + + -- | Common Errors | -- shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." @@ -189,7 +228,7 @@ typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array ---- Testing - +--update_ (fromList [100,200,300::Int]) (fromList [0,1::Int]) (fromList [2,4::Int]) -- Helper trying to simplify the type checking.... {- I think this is a dead end.... just do the case by case @@ -224,5 +263,7 @@ unwrapND (NdArray v s) = case typeOf x of nd1 :: NdArray nd2 :: NdArray +nd3 :: NdArray nd1 = NdArray (fromList [1,2,3::Int]) [3] nd2 = NdArray (fromList [10,11,12::Int]) [3] +nd3 = NdArray (fromList [2,4,8,16::Int]) [2,2] From d4042649c210111a338f894cf4cedc1f83012299 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 17 Jul 2023 17:45:55 +0100 Subject: [PATCH 21/90] many serialisation updates --- ndarray.cabal | 3 +- src/Serialisation.hs | 66 +++++++++++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/ndarray.cabal b/ndarray.cabal index 108eb76..34da61c 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -13,12 +13,13 @@ extra-doc-files: CHANGELOG.md -- extra-source-files: library - exposed-modules: Numskull + exposed-modules: Numskull, Serialisation -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 , dense , vector + , split hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 203af8a..c43626a 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -4,36 +4,49 @@ import Numskull import DType import System.IO -import Data.Vector.Storable as S +import qualified Data.Vector.Storable as S import Data.Word (Word16) import Data.List as List import Foreign (ForeignPtr, Ptr, alloca) import Foreign.Storable (poke, sizeOf) +import Data.List.Split + -- built in numpy serialisation descriptions getNumpyDType :: NdArray -> String -getNumpyDType _ = " " " " " " " " error "Non-standard types cannot be serialised. Yet." + getNumpyShape :: NdArray -> String -getNumpyShape _ = "(3,)" --3x1 explicitly for now +getNumpyShape (NdArray _ s) = "(" <> (take (length lshape -1) $ drop 1 $ lshape) <> ")" + where lshape = show s +getElemSize :: NdArray -> Int +getElemSize (NdArray v _) = S.maximum $ S.map sizeOf v -- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 -- see https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html saveNpy :: FilePath -> NdArray -> IO () -saveNpy path (NdArray v) = withBinaryFile path WriteMode $ \h -> do +saveNpy path (NdArray v s) = withBinaryFile path WriteMode $ \h -> do let - -- Unpack vector - nd = (NdArray v) + -- Unpacked specs + nd = (NdArray v s) + dtype = getNumpyDType nd + shape = getNumpyShape nd + vectorSize = (fromInteger $ product s) :: Int + elemSize = getElemSize nd -- Header string without length header = - "{'descr': '"<> getNumpyDType nd <> "', " <> - "'fortran_order': False, " <> - "'shape': "<> getNumpyShape nd <> " }" + "{'descr': '"<> dtype <> "', " <> + "'fortran_order': False, "<> + "'shape': "<> shape <> " }" -- Calculate header length (& padding) unpaddedLen = 6 + 2 + 2 + List.length header + 1 paddedLen = ((unpaddedLen + 63) `Prelude.div` 64) * 64 @@ -47,13 +60,28 @@ saveNpy path (NdArray v) = withBinaryFile path WriteMode $ \h -> do hPutChar h '\n' -- Put vector body --unsafeWithPtr a $ \ptr -> hPutBuf h ptr (size nd * sizeOf (undefined :: (NumpyType a)=>a)) - unsafeWith v (\ptr -> hPutBuf h ptr (3 * sizeOf (undefined :: Int))) - -saveNpz = undefined + S.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) -loadNpy = undefined +loadNpy :: FilePath -> IO () +loadNpy path = withBinaryFile path ReadMode $ \h -> do + descr <- hGetLine h + let + attrs = splitOn ":" $ (splitOn "{" descr) !! 1 + dtype = takeWhile (/='\'') $ drop 2 $ attrs !! 1 + shapeStrs = splitOn "," $ takeWhile (/=')') $ drop 2 $ attrs !! 3 + shape = map (\x -> read x :: Integer) $ take (length shapeStrs -1) shapeStrs + temp = sizeOf (undefined ::Int) + alloca $ \ptr -> do + buff <- hGetBuf h ptr temp + putStrLn $ show $ buff +-- alloca $ \ptr -> poke ptr (fromIntegral headerLen :: Word16) >> hPutBuf h ptr 2 loadNpz = undefined +saveNpz = undefined + testsave :: IO () -testsave = do saveNpy "./testout/idk.npy" (NdArray (fromList [1,2,3 :: Int])) \ No newline at end of file +testsave = do saveNpy "./testout/idk.npy" (NdArray (S.fromList [1,2,3 :: Int]) [3]) + +testload :: IO () +testload = do loadNpy "./src/testout/idk.npy" \ No newline at end of file From aa9f7ea800afbd1e2be75c7c28dc16778df47b80 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 18 Jul 2023 11:03:07 +0100 Subject: [PATCH 22/90] serialised numpy to haskell --- ndarray.cabal | 1 + src/Serialisation.hs | 45 +++++++++++++++++++++------ src/testout/{idk.npy => test123.npy} | Bin 3 files changed, 37 insertions(+), 9 deletions(-) rename src/testout/{idk.npy => test123.npy} (100%) diff --git a/ndarray.cabal b/ndarray.cabal index 34da61c..870bcfa 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -20,6 +20,7 @@ library , dense , vector , split + , binary hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall diff --git a/src/Serialisation.hs b/src/Serialisation.hs index c43626a..9a81a03 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -7,9 +7,10 @@ import System.IO import qualified Data.Vector.Storable as S import Data.Word (Word16) import Data.List as List -import Foreign (ForeignPtr, Ptr, alloca) -import Foreign.Storable (poke, sizeOf) +import Foreign (ForeignPtr, Ptr, alloca, mallocBytes) +import Foreign.Storable (poke, peek, sizeOf) +import Data.Binary import Data.List.Split -- built in numpy serialisation descriptions @@ -25,7 +26,7 @@ getNumpyDType (NdArray v _) = case show $ ty v of _ -> error "Non-standard types cannot be serialised. Yet." getNumpyShape :: NdArray -> String -getNumpyShape (NdArray _ s) = "(" <> (take (length lshape -1) $ drop 1 $ lshape) <> ")" +getNumpyShape (NdArray _ s) = "(" <> (drop 1 $ take (length lshape -1) $ lshape) <> ",)" where lshape = show s getElemSize :: NdArray -> Int @@ -62,7 +63,18 @@ saveNpy path (NdArray v s) = withBinaryFile path WriteMode $ \h -> do --unsafeWithPtr a $ \ptr -> hPutBuf h ptr (size nd * sizeOf (undefined :: (NumpyType a)=>a)) S.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) -loadNpy :: FilePath -> IO () +buffInt :: Handle -> IO Int +buffInt h = do + ptr <- mallocBytes 3 + hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined ::Int)) + val <- peek ptr + pure val + +buffInts :: Handle -> Integer -> [IO Int] +buffInts _ 0 = [] +buffInts h i = do (buffInt h) : buffInts h (i-1) + +loadNpy :: FilePath -> IO NdArray loadNpy path = withBinaryFile path ReadMode $ \h -> do descr <- hGetLine h let @@ -70,18 +82,33 @@ loadNpy path = withBinaryFile path ReadMode $ \h -> do dtype = takeWhile (/='\'') $ drop 2 $ attrs !! 1 shapeStrs = splitOn "," $ takeWhile (/=')') $ drop 2 $ attrs !! 3 shape = map (\x -> read x :: Integer) $ take (length shapeStrs -1) shapeStrs + size = product shape temp = sizeOf (undefined ::Int) - alloca $ \ptr -> do - buff <- hGetBuf h ptr temp - putStrLn $ show $ buff + let lio = buffInts h size + l <- traverse id lio + pure $ NdArray (S.fromList l :: S.Vector Int) shape + + {- + t <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) + t2 <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) + t3 <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) + --rest <- thing + let ts = show t ++ show t2 ++ show t3 + putStrLn $ ts -- alloca $ \ptr -> poke ptr (fromIntegral headerLen :: Word16) >> hPutBuf h ptr 2 +-} + +--f :: Handle -> Int -> IO Int +--f h temp = malloc $ \ptr -> hGetBuf h (ptr :: Ptr Int) (temp :: Int) loadNpz = undefined saveNpz = undefined testsave :: IO () -testsave = do saveNpy "./testout/idk.npy" (NdArray (S.fromList [1,2,3 :: Int]) [3]) +testsave = do saveNpy "./src/testout/test123.npy" (NdArray (S.fromList [1,2,3 :: Int]) [3]) testload :: IO () -testload = do loadNpy "./src/testout/idk.npy" \ No newline at end of file +testload = do + nd <- loadNpy "./src/testout/test123.npy" + putStrLn $ show $ nd \ No newline at end of file diff --git a/src/testout/idk.npy b/src/testout/test123.npy similarity index 100% rename from src/testout/idk.npy rename to src/testout/test123.npy From ebea44806a62c4aaf66539de60c514d5b633f056 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 18 Jul 2023 11:15:13 +0100 Subject: [PATCH 23/90] tidying --- ndarray.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/ndarray.cabal b/ndarray.cabal index 870bcfa..34da61c 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -20,7 +20,6 @@ library , dense , vector , split - , binary hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall From 57358bf4a1343b6873fb9f8a509a44ce94694683 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 18 Jul 2023 16:13:43 +0100 Subject: [PATCH 24/90] added git ignore --- src/Numskull.hs | 157 ++++++++++++++++++------------------------------ 1 file changed, 60 insertions(+), 97 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 810cc9c..de930d8 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -12,30 +12,39 @@ import Data.Vector.Storable as V import Data.Dynamic -- Not needed? import Type.Reflection --- | Typing shorthand | -- +-- $setup +-- >>> import Numskull as N + + +-- * Typing Shorthand +-- | typeOf synonym. ty :: Typeable a => a -> TypeRep a -ty x = typeOf x +ty = typeOf +-- | eqTypeRep synonym, returning Just HRefl in the case of type equality. (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) +-- * NdArray -- Todo: Should shapes be [Integer] or [Int] or maybe even another vector? --- NdArray -- --- The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are +-- | The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are -- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). data NdArray where - NdArray :: DType a => Vector a -> [Integer] -> NdArray + NdArray :: DType a => [Integer] -> Vector a -> NdArray + +fromList :: DType a => [Integer] -> [a] -> NdArray +fromList shape content = NdArray shape (V.fromList content) -- Todo: show in a nicer shapely form :) instance Show NdArray where - show (NdArray v s) = show v <> show s + show (NdArray s v) = show s <> show v instance Eq NdArray where - (NdArray v s) == (NdArray u r) = (r == s) && + (NdArray s v) == (NdArray r u) = (r == s) && case v =@= u of Just HRefl -> v == u Nothing -> False - (NdArray v s) /= (NdArray u r) = (r /= s) || + (NdArray s v) /= (NdArray r u) = (r /= s) || case v =@= u of Just HRefl -> v /= u Nothing -> True @@ -43,12 +52,12 @@ instance Eq NdArray where -- Investigate how Vector implements further -- Todo: check max and min work properly on all dtypes, probably use a map instead instance Ord NdArray where - (NdArray v s) `compare` (NdArray u r) = if s == r then case v =@= u of + (NdArray s v) `compare` (NdArray r u) = if s == r then case v =@= u of Just HRefl -> compare v u Nothing -> error $ typeMismatch (show v) (show u) else error $ shapeMismatch (show s) (show r) - (NdArray v s) <= (NdArray u r) = if s == r then case v =@= u of + (NdArray s v) <= (NdArray r u) = if s == r then case v =@= u of Just HRefl -> v <= u Nothing -> error $ typeMismatch (show v) (show u) else error $ shapeMismatch (show s) (show r) @@ -63,10 +72,13 @@ instance Num NdArray where (+) = pointwiseZip add (-) = pointwiseZip DType.subtract (*) = undefined -- Matrix multiplication - negate (NdArray v s) = NdArray (V.map DType.invert v) s - abs (NdArray v s) = NdArray (V.map DType.abs v) s - signum (NdArray v s) = NdArray (V.map DType.signum v) s - fromInteger x = NdArray (singleton $ fromInteger @Int x) [1] + negate (NdArray s v) = NdArray s (V.map DType.invert v) + abs (NdArray v s) = NdArray s (V.map DType.abs v) + signum (NdArray v s) = NdArray s (V.map DType.signum v) + fromInteger x = NdArray [1] (singleton $ fromInteger @Int x) + +size :: [Integer] -> Int +size shape = (fromIntegral $ P.product shape) :: Int -- | Creation | -- -- Todo: get the ident of the dtype from an nd array @@ -76,8 +88,14 @@ indentityElem' :: forall a . DType a => Vector a -> a indentityElem' _ = DType.identity :: DType a => a -- Todo: Create ident array of certain shape -zeros = undefined - +{- +zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray +zeros t s = NdArray zerovec s + where + ident = (DType.identity :: DType a => a) + zerovec = (V.replicate (size s) ident) :: DType a => Vector a +-} + -- | Indexing & Slicing | -- -- Since vectors are 1D arrays but the matricicies can have n-dimensions, index conversion is neccessary -- The index i will be the 1D index @@ -117,9 +135,9 @@ expandInd shape i = expandRun shape i 1 -- Two Arguments -- https://hackage.haskell.org/package/base-4.18.0.0/docs/Control-Monad-ST.html#v:runST pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray -pointwiseZip zipfunc (NdArray v s) (NdArray u r) = if s == r then +pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> NdArray (V.zipWith zipfunc v u) s -- Types match + Just HRefl -> NdArray s (V.zipWith zipfunc v u) -- Types match Nothing -> error $ typeMismatch (show$ty v) (show$ty u) else error $ shapeMismatch (show s) (show r) @@ -147,38 +165,35 @@ elemPow = pointwiseZip pow -- Use the TypeApplications syntax: -- case typeOf x `eqTypeRep` typeRep @Integer of matchDType :: NdArray -> NdArray -> Maybe NdArray -matchDType (NdArray v _) (NdArray u r) = case v =@= fromList [1::Int] of - Just HRefl -> Just $ NdArray (V.map dtypeToInt u) r +matchDType (NdArray _ v) (NdArray r u) = case v =@= V.fromList [1::Int] of + Just HRefl -> Just $ NdArray r (V.map dtypeToInt u) _ -> Nothing -- Check that the matrix isn't larger than the shape but if so truncate it -constrainSize :: DType a => Vector a -> [Integer] -> (Bool, Vector a) -constrainSize v s = - if size < len then (False, V.take size v) +constrainSize :: DType a => [Integer] -> Vector a -> (Bool, Vector a) +constrainSize s v = + if (size s) < len then (False, V.take (size s) v) else (True, v) - where - size = fromInteger (P.product s) :: Int + where len = V.length v -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) -padSize :: DType a => Vector a -> [Integer] -> Vector a -padSize v s = v V.++ V.replicate (size - len) identity - where - size = fromInteger (P.product s) :: Int - len = V.length v +padSize :: DType a => [Integer] -> Vector a -> Vector a +padSize s v = v V.++ V.replicate ((size s) - len) identity + where len = V.length v -- Contrain or pad the vector to match the size -setSize :: DType a => Vector a -> [Integer] -> Vector a -setSize v s = let (unchanged, u) = constrainSize v s in - if unchanged then padSize u s else u +setSize :: DType a => [Integer] -> Vector a -> Vector a +setSize s v = let (unchanged, u) = constrainSize s v in + if unchanged then padSize s u else u -- Constrain or pad the NdArray to match the new given size resize :: NdArray -> [Integer] -> NdArray -resize (NdArray v _) r = NdArray (setSize v r) r +resize (NdArray _ v) r = NdArray (setSize r v) r reshape :: NdArray -> [Integer] -> Maybe NdArray -reshape (NdArray v s) r = if P.product s == P.product r - then Just $ NdArray v r +reshape (NdArray s v) r = if P.product s == P.product r + then Just $ NdArray r v else Nothing --NB: reshape will pad/truncate individual dimensions whereas resize keeps as many values as possible but they might switch position @@ -186,34 +201,27 @@ reshape (NdArray v s) r = if P.product s == P.product r map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer map1DIndex s r i = collapseInd r (expandInd s i) --- ok then what im gonna do is make an array of all the mapping and value pairs and // it - mapShapeLoss :: [Integer] -> [Integer] -> Bool mapShapeLoss s r = if P.length r < P.length s then True else P.or $ P.zipWith (>) s r -{- -hack :: forall a . DType a => a -> a -hack _ = (DType.identity :: DType a => a) - -vectorHack :: forall a . DType a => Vector a -> Vector a -vectorHack _ = singleton (DType.identity :: DType a => a) - -vectorHack' :: forall a . DType a => Vector a -> a -vectorHack' _ = DType.identity :: DType a => a --} - -- If you try to map to a smaller shape, values are dropped & weird stuff happens, otherwise padded + +-- | Some helpful documentation +-- +-- >>> padShape (Numskull.fromList [2] [3,4 :: Int]) [5] +-- [3,4,0,0,0][5] +-- padShape :: NdArray -> [Integer] -> NdArray -padShape (NdArray v s) r = +padShape (NdArray s v) r = let newSize = fromInteger @Int (P.product r) nullVec = V.replicate newSize (indentityElem' v) fi i = fromIntegral @Int @Integer i - newIndicies = imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v + newIndices = imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v in - NdArray (unsafeUpdate_ nullVec newIndicies v) r + NdArray (unsafeUpdate_ nullVec newIndices v) r -- | Common Errors | -- @@ -222,48 +230,3 @@ shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with arr typeMismatch :: String -> String -> String typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array of type '" <> t2 <> "'." - - - - - ----- Testing ---update_ (fromList [100,200,300::Int]) (fromList [0,1::Int]) (fromList [2,4::Int]) --- Helper trying to simplify the type checking.... - -{- I think this is a dead end.... just do the case by case -eqDType :: (Typeable a, Typeable b) => a -> b -> Bool -eqDType x y = case eqTypeRep (typeOf x) (typeOf y) of - Just HRefl -> True - _ -> False - -equey :: NdArray -> NdArray -> Vector Bool -equey (NdArray v1 s1) (NdArray v2 s2) = - if eqDType (NdArray v1 s1) (NdArray v2 s2) - then V.zipWith (==) v1 v2 - else (fromList [True]) :: Vector Bool --} - -{- Pointwise zip with type conversion (TODO) -pointwiseZip (NdArray v s) (NdArray u r) = case (eqTypeRep xtype ytype, matchDType (NdArray v s) (NdArray u r)) of - (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - -- Code to auto-cast types - --(_, Just casted) -> (NdArray v s) + casted -- Second type can be converted to first - _ -> error ("Cannot match second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") - where - xtype = ty x; ytype = ty y --} - -{- -unwrapND :: NdArray -> (String, Vector Dynamic) -unwrapND (NdArray v s) = case typeOf x of - vecTypeInt -> ("Int", V.map toDyn x) - vecTypeBool -> ("Bool", V.map toDyn x) --} - -nd1 :: NdArray -nd2 :: NdArray -nd3 :: NdArray -nd1 = NdArray (fromList [1,2,3::Int]) [3] -nd2 = NdArray (fromList [10,11,12::Int]) [3] -nd3 = NdArray (fromList [2,4,8,16::Int]) [2,2] From 402b4c028f4593e59d8ac91d1e6619abc1d5d864 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 18 Jul 2023 17:36:08 +0100 Subject: [PATCH 25/90] tidying --- .gitignore | 1 + ndarray.cabal | 31 +++++++++-- ndarray.nix | 9 +++- src/DType.hs | 107 ++++++++++++------------------------- src/Serialisation.hs | 60 ++++++++++----------- test/DocTest.hs | 11 ++++ test/Test.hs | 13 +++++ test/Test/Numskull.hs | 30 +++++++++++ test/Test/Serialisation.hs | 7 +++ 9 files changed, 160 insertions(+), 109 deletions(-) create mode 100644 .gitignore create mode 100644 test/DocTest.hs create mode 100644 test/Test.hs create mode 100644 test/Test/Numskull.hs create mode 100644 test/Test/Serialisation.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daa9e81 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist-newstyle/ \ No newline at end of file diff --git a/ndarray.cabal b/ndarray.cabal index 34da61c..3eaf4fb 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -9,17 +9,42 @@ author: Rowan Mather maintainer: rowan@myrtle.ai -- copyright: build-type: Simple -extra-doc-files: CHANGELOG.md +-- extra-doc-files: CHANGELOG.md -- extra-source-files: library - exposed-modules: Numskull, Serialisation + import library-deps + exposed-modules: Numskull, Serialisation, DType -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 - , dense , vector , split hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall + +test-suite test + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Test.hs + other-modules: Test.Numskull + Test.Serialisation + build-depends: base >=4.13.0.0 + , hspec + , QuickCheck + , ndarray + ghc-options: -Wall + default-language: Haskell2010 + +test-suite doctest + import library-deps + type: exitcode-stdio-1.0 + hs-source-dirs: test, src + main-is: DocTest.hs + build-depends: base >=4.13.0.0 + , doctest + , vector + , split + ghc-options: -Wall + default-language: Haskell2010 diff --git a/ndarray.nix b/ndarray.nix index cfbd8c7..4820eb1 100644 --- a/ndarray.nix +++ b/ndarray.nix @@ -1,8 +1,13 @@ -{ mkDerivation, base, dense, lib }: +{ mkDerivation, base, doctest, hspec, lib, QuickCheck, split +, vector +}: mkDerivation { pname = "ndarray"; version = "0.1.0.0"; src = ./.; - libraryHaskellDepends = [ base dense ]; + libraryHaskellDepends = [ base split vector ]; + testHaskellDepends = [ + base doctest hspec QuickCheck split vector + ]; license = lib.licenses.mit; } diff --git a/src/DType.hs b/src/DType.hs index 9607f35..964230d 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -9,7 +9,7 @@ import GHC.Float (float2Double) -- Basis for all pointwise operations class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where - identity :: a + identity :: a -- Numeric add :: a -> a -> a subtract :: a -> a -> a @@ -32,48 +32,10 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where invert :: a -> a shiftleft :: a -> a shiftright :: a -> a - -- Standard Conversions - dtypeToInt :: a -> Int - --dtypeToFloat :: a -> Float - --dtypeToDouble :: a -> Double - --dtypeToBool :: a -> Bool + -- Casting + dtypeToRational :: a -> Rational + rationalToDtype :: Rational -> a -{- ---INSTANCE TEMPLATE-- -instance DType TYPE where - -- Numeric - add x y = - subtract x y = - multiply x y = - divide x y = double - div x y = - power x d = double - pow x y = - log x y = - mod x y = integer - abs x = - ceil x = - floor x = - -- Trig - sin x = - cos x = - tan x = - -- Logical - invert x = - shiftleft x = - shiftright x = - -- Comparative - eq x y = - leq x y = - geq x y = - less x y = - greater x y = - -- Standard Conversions - dtypeToInt x = - dtypeToFloat x = - dtypeToDouble x = - dtypeToBool x = --} instance DType Int where identity = 0 @@ -85,24 +47,28 @@ instance DType Int where div = P.div power x d = fromIntegral x ** d pow x y = x ^ y - log x y = (P.floor (logBase ((fromIntegral x) :: Double) ((fromIntegral y) :: Double))) :: Int + log x y = (P.floor $ logBase xd yd) :: Int + where xd = fromIntegral @Int @Double x + yd = fromIntegral @Int @Double y mod x y = fromIntegral (x `P.mod` y) :: Integer abs = P.abs signum = P.signum ceil x = x floor x = x -- Trig - sin x = (round $ P.sin $ iTf x) :: Int - cos x = (round $ P.cos $ iTf x) :: Int - tan x = (round $ P.tan $ iTf x) :: Int + sin = roundIntFunc P.sin + cos = roundIntFunc P.cos + tan = roundIntFunc P.tan -- Logical invert x = -x shiftleft x = x * 2 shiftright x = x `P.div` 2 -- (Conversions) - dtypeToInt x = x + dtypeToRational = toRational + rationalToDtype = floor . fromRational @Double -iTf = fromIntegral @Int @Float +roundIntFunc :: (Float -> Float) -> Int -> Int +roundIntFunc f x = (round $ f $ fromIntegral @Int @Float x) :: Int --instance DType Int64 where? @@ -118,11 +84,12 @@ instance DType Float where power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y - mod x y = (fromIntegral (P.floor x `P.mod` P.floor y)) :: Integer + mod x y = (fromIntegral (xi `P.mod` yi)) :: Integer + where xi = P.floor x; yi = P.floor y abs = P.abs signum = P.signum - ceil x = (fromIntegral $ P.ceiling x) :: Float - floor x = (fromIntegral $ P.floor x) :: Float + ceil = fromIntegral @Float . P.ceiling + floor = fromIntegral @Float . P.floor -- Trig sin = P.sin cos = P.cos @@ -131,8 +98,9 @@ instance DType Float where invert x = -x shiftleft x = x * 2 shiftright x = x / 2 - -- (Conversions) - dtypeToInt x = (P.floor x) :: Int + -- Conversion + dtypeToRational = toRational + rationalToDtype = fromRational @Float --instance DType Double @@ -140,10 +108,10 @@ instance DType Bool where identity = False -- Numeric add x y = x || y - subtract x y = toEnum (fromEnum x - fromEnum y) + subtract x y = (x || y) && not (x && y) multiply x y = x && y divide _x _y = undefined - div x y = (x || y) && not (x && y) + div x y = not (x && y) power _x _d = undefined pow x y = toEnum (fromEnum x ^ fromEnum y) log _x _y = undefined @@ -152,27 +120,22 @@ instance DType Bool where signum = id ceil = id floor = id - -- Trig - sin = boolEnumOp P.sin - cos = boolEnumOp P.cos - tan = boolEnumOp P.tan + -- Trig (False = 0, True = 1 or /=0) + sin False = False + sin True = True + cos False = True + cos True = True + tan False = False + tan True = True -- Logical invert x = not x shiftleft _ = False shiftright _ = False - -- Comparative - {- - eq x y = x == y - leq x y = x <= y - geq x y = x >= y - less x y = x < y - greater x y = x > y - -} - -- (Conversions) - dtypeToInt x = (fromEnum x) :: Int - -boolEnumOp :: (RealFrac a1 , Num a2) => (a2 -> a1) -> Bool -> Bool -boolEnumOp f x = 0 /= ((round $ f $ fromIntegral $ fromEnum x) :: Integer) + -- Conversions + dtypeToRational False = 0 + dtypeToRational True = 1 + rationalToDtype 0 = False + rationalToDtype _ True --instance DType Char where? diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 9a81a03..44d00a2 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -4,18 +4,18 @@ import Numskull import DType import System.IO -import qualified Data.Vector.Storable as S -import Data.Word (Word16) import Data.List as List -import Foreign (ForeignPtr, Ptr, alloca, mallocBytes) +import Data.List.Split +import qualified Data.Vector.Storable as S +import Foreign (Ptr, alloca, mallocBytes) import Foreign.Storable (poke, peek, sizeOf) +import Data.Word (Word16) -import Data.Binary -import Data.List.Split +-- | HASKELL TO PYTHON | -- --- built in numpy serialisation descriptions +-- Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String -getNumpyDType (NdArray v _) = case show $ ty v of +getNumpyDType (NdArray _ v) = case show $ ty v of "Vector Int" -> " " " " error "Non-standard types cannot be serialised. Yet." +-- Converts shape list to a string of the Numpy tuple form e.g. (3,2,) getNumpyShape :: NdArray -> String -getNumpyShape (NdArray _ s) = "(" <> (drop 1 $ take (length lshape -1) $ lshape) <> ",)" +getNumpyShape (NdArray s _) = "(" <> (drop 1 $ take (length lshape -1) $ lshape) <> ",)" where lshape = show s +-- Maximum memory required for any single element in the array getElemSize :: NdArray -> Int -getElemSize (NdArray v _) = S.maximum $ S.map sizeOf v +getElemSize (NdArray _ v) = S.maximum $ S.map sizeOf v +-- Saves any of the standard types defined above as a .npy -- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 -- see https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html saveNpy :: FilePath -> NdArray -> IO () -saveNpy path (NdArray v s) = withBinaryFile path WriteMode $ \h -> do +saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do let -- Unpacked specs - nd = (NdArray v s) + nd = (NdArray s v) dtype = getNumpyDType nd shape = getNumpyShape nd vectorSize = (fromInteger $ product s) :: Int @@ -60,13 +63,16 @@ saveNpy path (NdArray v s) = withBinaryFile path WriteMode $ \h -> do hPutStr h (List.replicate padding ' ') hPutChar h '\n' -- Put vector body - --unsafeWithPtr a $ \ptr -> hPutBuf h ptr (size nd * sizeOf (undefined :: (NumpyType a)=>a)) S.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) + +-- | PYTHON TO HASKELL | -- + +-- Reads an Int value from the binary accessed via the handle buffInt :: Handle -> IO Int buffInt h = do - ptr <- mallocBytes 3 - hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined ::Int)) + ptr <- mallocBytes 4 + _ <- hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined ::Int)) val <- peek ptr pure val @@ -74,8 +80,11 @@ buffInts :: Handle -> Integer -> [IO Int] buffInts _ 0 = [] buffInts h i = do (buffInt h) : buffInts h (i-1) +-- NB: Currently only works with Ints specifically but this is easy to extend :) Working on it +-- Loads an NdArray from a .npy file loadNpy :: FilePath -> IO NdArray loadNpy path = withBinaryFile path ReadMode $ \h -> do + -- Unpacks and parses the header to get the array type and size descr <- hGetLine h let attrs = splitOn ":" $ (splitOn "{" descr) !! 1 @@ -83,30 +92,17 @@ loadNpy path = withBinaryFile path ReadMode $ \h -> do shapeStrs = splitOn "," $ takeWhile (/=')') $ drop 2 $ attrs !! 3 shape = map (\x -> read x :: Integer) $ take (length shapeStrs -1) shapeStrs size = product shape - temp = sizeOf (undefined ::Int) + -- Reads the array itself into a list let lio = buffInts h size l <- traverse id lio - pure $ NdArray (S.fromList l :: S.Vector Int) shape - - {- - t <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) - t2 <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) - t3 <- alloca (\ptr -> hGetBuf h (ptr :: Ptr Int) temp) - --rest <- thing - let ts = show t ++ show t2 ++ show t3 - putStrLn $ ts --- alloca $ \ptr -> poke ptr (fromIntegral headerLen :: Word16) >> hPutBuf h ptr 2 --} - ---f :: Handle -> Int -> IO Int ---f h temp = malloc $ \ptr -> hGetBuf h (ptr :: Ptr Int) (temp :: Int) + -- Converts the list & shape to an NdArray + pure $ NdArray shape (S.fromList l :: S.Vector Int) -loadNpz = undefined -saveNpz = undefined +-- Try it! It will probably break easily testsave :: IO () -testsave = do saveNpy "./src/testout/test123.npy" (NdArray (S.fromList [1,2,3 :: Int]) [3]) +testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (S.fromList [1,2,3 :: Int]) ) testload :: IO () testload = do diff --git a/test/DocTest.hs b/test/DocTest.hs new file mode 100644 index 0000000..4019468 --- /dev/null +++ b/test/DocTest.hs @@ -0,0 +1,11 @@ +module Main where + +-- doctest +import Test.DocTest + +main :: IO () +main = doctest $ "-isrc" : map ("src/" <>) + [ "DType.hs" + , "Numskull.hs" + , "Serialisation.hs" + ] \ No newline at end of file diff --git a/test/Test.hs b/test/Test.hs new file mode 100644 index 0000000..15d55c1 --- /dev/null +++ b/test/Test.hs @@ -0,0 +1,13 @@ +module Main where + +-- hspec +import Test.Hspec + +-- ndarray (local) +import qualified Test.Numskull +import qualified Test.Serialisation + +main :: IO () +main = hspec $ do + describe "Test.Numskull" Test.Numskull.spec + describe "Test.Serialisation" Test.Serialisation.spec \ No newline at end of file diff --git a/test/Test/Numskull.hs b/test/Test/Numskull.hs new file mode 100644 index 0000000..7863c5d --- /dev/null +++ b/test/Test/Numskull.hs @@ -0,0 +1,30 @@ +module Test.Numskull where + +-- hspec +import Test.Hspec + +-- QuickCheck +import Test.QuickCheck (NonNegative(..), property) + +-- ndarray (local) +import Numskull as N + +spec :: Spec +spec = do + describe "NdArray equality" $ + it "works" $ + N.fromList [3] [1,2,3::Int] == N.fromList [3] [1,2,3::Int] + + describe "padShape" $ do + focus . it "works on a less simple example" $ + property $ \content (NonNegative extra) -> + let n = toInteger $ length content + in + padShape (N.fromList [n] content) [n + extra] `shouldBe` N.fromList [n + extra] (content <> replicate (fromInteger extra) (0 :: Int)) + +--cabal test --test-show-details=streaming +-- ghci -isrc -itest test/Test/Numskull.hs +-- ghci> hspec spec + + +-- https://hackage.haskell.org/package/hspec-2.11.3/docs/Test-Hspec.html#v:example diff --git a/test/Test/Serialisation.hs b/test/Test/Serialisation.hs new file mode 100644 index 0000000..4b497c6 --- /dev/null +++ b/test/Test/Serialisation.hs @@ -0,0 +1,7 @@ +module Test.Serialisation where + +-- hspec +import Test.Hspec + +spec :: Spec +spec = pure () \ No newline at end of file From 4b040cefe14351f6c00b9688aa0c6541f31facd0 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 19 Jul 2023 17:39:57 +0100 Subject: [PATCH 26/90] changed package name & added matrix --- README.md | 6 +-- default.nix | 2 +- ndarray.cabal | 5 ++- ndarray.nix => numskull.nix | 2 +- src/DType.hs | 11 +++-- src/MatrixForm.hs | 34 ++++++++++++++ src/Numskull.hs | 88 ++++++++++++++++++++++++++++--------- test/Test/Numskull.hs | 2 + 8 files changed, 116 insertions(+), 34 deletions(-) rename ndarray.nix => numskull.nix (92%) create mode 100644 src/MatrixForm.hs diff --git a/README.md b/README.md index 85a31eb..ba05c4d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rowan-ndarray +# rowan-numskull ## Development @@ -19,10 +19,10 @@ Source repositories specified in `cabal.project` should be kept up to date with #### cabal2nix -`ndarray.nix` should be updated with +`numskull.nix` should be updated with ```sh -$ cabal2nix . > ndarray.nix +$ cabal2nix . > numskull.nix ``` whenever the Cabal file is updated with e.g. new dependencies. diff --git a/default.nix b/default.nix index 579d269..59d4cd8 100644 --- a/default.nix +++ b/default.nix @@ -1,2 +1,2 @@ { nixpkgs ? import nix/nixpkgs.nix {} }: -nixpkgs.pkgs.haskellPackages.callPackage ./ndarray.nix { } +nixpkgs.pkgs.haskellPackages.callPackage ./numskull.nix { } diff --git a/ndarray.cabal b/ndarray.cabal index 3eaf4fb..b16774e 100644 --- a/ndarray.cabal +++ b/ndarray.cabal @@ -1,5 +1,5 @@ cabal-version: 2.4 -name: ndarray +name: numskull version: 0.1.0.0 -- synopsis: -- description: @@ -20,6 +20,7 @@ library build-depends: base >=4.13.0.0 , vector , split + , containers hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall @@ -33,7 +34,7 @@ test-suite test build-depends: base >=4.13.0.0 , hspec , QuickCheck - , ndarray + , numskull ghc-options: -Wall default-language: Haskell2010 diff --git a/ndarray.nix b/numskull.nix similarity index 92% rename from ndarray.nix rename to numskull.nix index 4820eb1..8212149 100644 --- a/ndarray.nix +++ b/numskull.nix @@ -2,7 +2,7 @@ , vector }: mkDerivation { - pname = "ndarray"; + pname = "numskull"; version = "0.1.0.0"; src = ./.; libraryHaskellDepends = [ base split vector ]; diff --git a/src/DType.hs b/src/DType.hs index 964230d..8de38d7 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -65,13 +65,12 @@ instance DType Int where shiftright x = x `P.div` 2 -- (Conversions) dtypeToRational = toRational - rationalToDtype = floor . fromRational @Double + rationalToDtype = P.floor . fromRational @Double roundIntFunc :: (Float -> Float) -> Int -> Int roundIntFunc f x = (round $ f $ fromIntegral @Int @Float x) :: Int - ---instance DType Int64 where? +--instance DType Int64 where instance DType Float where identity = 0.0 @@ -88,8 +87,8 @@ instance DType Float where where xi = P.floor x; yi = P.floor y abs = P.abs signum = P.signum - ceil = fromIntegral @Float . P.ceiling - floor = fromIntegral @Float . P.floor + ceil = fromIntegral @Integer @Float . P.ceiling + floor = fromIntegral @Integer @Float . P.floor -- Trig sin = P.sin cos = P.cos @@ -135,7 +134,7 @@ instance DType Bool where dtypeToRational False = 0 dtypeToRational True = 1 rationalToDtype 0 = False - rationalToDtype _ True + rationalToDtype _ = True --instance DType Char where? diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs new file mode 100644 index 0000000..737ffeb --- /dev/null +++ b/src/MatrixForm.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE TypeApplications #-} + +module MatrixForm where + +import Data.Tree + +data TreeMatrix a = B a | A [TreeMatrix a] + +matrixToTree :: TreeMatrix a -> Tree [a] +matrixToTree (B x) = Node [x] [] +matrixToTree (A xs) = Node [] (map matrixToTree xs) + +-- Example 2x3x2 +l = A [A [A [B 1, B 2], + A [B 3, B 4], + A [B 5, B 6]], + + A [A [B 7, B 8], + A [B 9, B 10], + A [B 11, B 12]]] + +-- Prelude.map Prelude.length $ levels $ treeify l'' +-- dimension = next val/current val +-- Prelude.zipWith div (Prelude.drop 1 x) x + +flattenToList :: Tree [a] -> [a] +flattenToList = concat . flatten + +treeShape :: Tree [a] -> [Integer] +treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLen) levelLen + where levelLen = map length $ levels t + +matrixShape :: TreeMatrix a -> [Integer] +matrixShape = treeShape . matrixToTree \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index de930d8..787a222 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -6,6 +6,7 @@ module Numskull where import DType +import MatrixForm import Prelude as P import Data.Vector.Storable as V @@ -22,6 +23,10 @@ ty :: Typeable a => a -> TypeRep a ty = typeOf -- | eqTypeRep synonym, returning Just HRefl in the case of type equality. +-- >>> case True =@= False of +-- >>> Just HRefl -> putStrLn "Two Booleans will match" +-- >>> Nothing -> putStrLn "Mismatching types" +-- Two Booleans will match (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) @@ -32,9 +37,6 @@ ty = typeOf data NdArray where NdArray :: DType a => [Integer] -> Vector a -> NdArray -fromList :: DType a => [Integer] -> [a] -> NdArray -fromList shape content = NdArray shape (V.fromList content) - -- Todo: show in a nicer shapely form :) instance Show NdArray where show (NdArray s v) = show s <> show v @@ -77,16 +79,31 @@ instance Num NdArray where signum (NdArray v s) = NdArray s (V.map DType.signum v) fromInteger x = NdArray [1] (singleton $ fromInteger @Int x) + +-- * Creation & Miscellaneous + +-- | Gets the total number of elements in a given array shape. size :: [Integer] -> Int size shape = (fromIntegral $ P.product shape) :: Int --- | Creation | -- +fromList :: DType a => [Integer] -> [a] -> NdArray +fromList shape l = + if length l /= size shape then error "Length of the list should match the total number of elements specified by the shape." + else NdArray shape (V.fromList l) + +fromListFlat :: DType a => [Integer] -> [a] -> NdArray +fromList l = NdArray [genericLength @Integer l] (V.fromList l) + +fromMatrix :: TreeMatrix -> NdArray +fromMatrix m = NdArray (matrixShape m) (V.fromList $ flattenToList m) + -- Todo: get the ident of the dtype from an nd array indentityElem = undefined indentityElem' :: forall a . DType a => Vector a -> a indentityElem' _ = DType.identity :: DType a => a + -- Todo: Create ident array of certain shape {- zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray @@ -96,35 +113,65 @@ zeros t s = NdArray zerovec s zerovec = (V.replicate (size s) ident) :: DType a => Vector a -} --- | Indexing & Slicing | -- --- Since vectors are 1D arrays but the matricicies can have n-dimensions, index conversion is neccessary --- The index i will be the 1D index --- Then x y z... for each dimension index --- i = x + y*xsize + z*xsize*ysize + ... --- x = i % xsize; y = i/(xsize) % ysize; z = i/(xsize*ysize) % zsize; ... --- As described: https://softwareengineering.stackexchange.com/questions/212808/treating-a-1d-data-structure-as-2d-grid +-- * Indexing & Slicing +{- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, +we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the +shape of the array, [sx,sy,sz,...], as follows: + + i = x + y*sx + z*sx*sy + ... + + x = i/(1) % sx + y = i/(sx) % sy + z = i/(sx*sy) % sz + ... + +-} + +-- | Converts a shape and multi-index to a 1D index. +collapseInd :: [Integer] -> [Integer] -> Integer +collapseInd shape indicies = collapseRun shape indicies 1 --- helper for collapseInd --- can this be folded? its over two things so i dont think so... only if i zip it +-- Helper for collapseInd collapseRun :: [Integer] -> [Integer] -> Integer -> Integer collapseRun _ [] _ = 0 collapseRun [] _ _ = 0 collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) -collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd shape indicies = collapseRun shape indicies 1 +-- | Converts a shape and 1D index to a multi-index. +expandInd :: [Integer] -> Integer -> [Integer] +expandInd shape i = expandRun shape i 1 --- helper for expandInd +-- Helper for expandInd expandRun :: [Integer] -> Integer -> Integer -> [Integer] expandRun [] _ _ = [] expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) where x = (i `P.div` runSize) `P.mod` s -expandInd :: [Integer] -> Integer -> [Integer] -expandInd shape i = expandRun shape i 1 +-- | Checks an index does not exceed the shape +validIndex :: NdArray -> [Integer] -> Bool +validIndex (NdArray s v) i = (length s == length v) && and $ zipWith lessAbs s i + where lessAbs x y = (y>=0 && y [Integer] -> Dtype +(NdArray s v) #! i = case (NdArray s v) !? i of + Just val -> val + Nothing -> indentityElem' v + +{- | The safer version of #! which returns Nothing if an index exceeds the shape bounds. -} +(!?) :: NdArray -> [Integer] -> Maybe Dtype +(NdArray s v) !? i = + let + valid = validIndex (NdArray s v) i + positives = zipWith (\x y -> if y < 0 then x+y else y) s i + in + if valid then Just $ v ! (collapseInd s positives) else Nothing --- The actual indexing bit todo --- Slicing Todo :) +-- Todo: slicing -- | Pointwise Functions | -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances @@ -223,7 +270,6 @@ padShape (NdArray s v) r = in NdArray (unsafeUpdate_ nullVec newIndices v) r - -- | Common Errors | -- shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." diff --git a/test/Test/Numskull.hs b/test/Test/Numskull.hs index 7863c5d..ab5590e 100644 --- a/test/Test/Numskull.hs +++ b/test/Test/Numskull.hs @@ -28,3 +28,5 @@ spec = do -- https://hackage.haskell.org/package/hspec-2.11.3/docs/Test-Hspec.html#v:example + +-- https://hspec.github.io/ From 204957c6c4efb9abf922ce13c084e4a90b6b18b9 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 20 Jul 2023 12:31:03 +0100 Subject: [PATCH 27/90] more creation & indexing that works --- src/DType.hs | 4 +- src/MatrixForm.hs | 2 + src/Numskull.hs | 135 ++++++++++++++++++++++++++++++---------------- 3 files changed, 93 insertions(+), 48 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 8de38d7..5c65ca2 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -79,11 +79,11 @@ instance DType Float where subtract x y = x - y multiply x y = x * y divide x y = float2Double (x / y) - div x y = (fromIntegral (P.floor x `P.div` P.floor y)) :: Float + div x y = fromIntegral (P.floor x `P.div` P.floor y) :: Float power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y - mod x y = (fromIntegral (xi `P.mod` yi)) :: Integer + mod x y = fromIntegral (xi `P.mod` yi):: Integer where xi = P.floor x; yi = P.floor y abs = P.abs signum = P.signum diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index 737ffeb..eed50e3 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -11,6 +11,7 @@ matrixToTree (B x) = Node [x] [] matrixToTree (A xs) = Node [] (map matrixToTree xs) -- Example 2x3x2 +{- l = A [A [A [B 1, B 2], A [B 3, B 4], A [B 5, B 6]], @@ -18,6 +19,7 @@ l = A [A [A [B 1, B 2], A [A [B 7, B 8], A [B 9, B 10], A [B 11, B 12]]] +-} -- Prelude.map Prelude.length $ levels $ treeify l'' -- dimension = next val/current val diff --git a/src/Numskull.hs b/src/Numskull.hs index 787a222..432fef1 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -2,15 +2,16 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} module Numskull where -import DType +import qualified DType +import DType (DType) import MatrixForm -import Prelude as P -import Data.Vector.Storable as V -import Data.Dynamic -- Not needed? +import qualified Data.Vector.Storable as V +import Data.Vector.Storable (Vector) import Type.Reflection -- $setup @@ -39,9 +40,10 @@ data NdArray where -- Todo: show in a nicer shapely form :) instance Show NdArray where - show (NdArray s v) = show s <> show v + show (NdArray s v) = show s <> " " <> show v instance Eq NdArray where + -- | Arrays are equal if their elements and shape exactly match. (NdArray s v) == (NdArray r u) = (r == s) && case v =@= u of Just HRefl -> v == u @@ -51,9 +53,10 @@ instance Eq NdArray where Just HRefl -> v /= u Nothing -> True --- Investigate how Vector implements further --- Todo: check max and min work properly on all dtypes, probably use a map instead instance Ord NdArray where + {- | Arrays are only comparable when they are the same shape. Then they are + ordered by pointwise comparison. + -} (NdArray s v) `compare` (NdArray r u) = if s == r then case v =@= u of Just HRefl -> compare v u Nothing -> error $ typeMismatch (show v) (show u) @@ -63,48 +66,69 @@ instance Ord NdArray where Just HRefl -> v <= u Nothing -> error $ typeMismatch (show v) (show u) else error $ shapeMismatch (show s) (show r) - -- (>) (NdArray v s) (NdArray u r) = u > v - -- (>=) (NdArray v s) (NdArray u r) = u >= v - -- max (NdArray v s) = V.maximum v - -- min (NdArray v s) = V.minimum v -- To do: matrix multiplication :O -- To do: change fromInteger to return an integer array rather than int instance Num NdArray where - (+) = pointwiseZip add + -- | Adds elements pointwise + (+) = pointwiseZip DType.add + -- | Subtracts elements pointwise (-) = pointwiseZip DType.subtract - (*) = undefined -- Matrix multiplication + -- | Matrix multiplication TODO + (*) = undefined + -- | Inverts all elements according to their DType instance negate (NdArray s v) = NdArray s (V.map DType.invert v) - abs (NdArray v s) = NdArray s (V.map DType.abs v) - signum (NdArray v s) = NdArray s (V.map DType.signum v) - fromInteger x = NdArray [1] (singleton $ fromInteger @Int x) - + -- | Absolute value of each element + abs (NdArray s v) = NdArray s (V.map DType.abs v) + -- | Signum of each element + signum (NdArray s v) = NdArray s (V.map DType.signum v) + -- Creates a singleton array + fromInteger = singleton . fromInteger @Int -- * Creation & Miscellaneous -- | Gets the total number of elements in a given array shape. +-- >>> size [2,3] +-- 6 size :: [Integer] -> Int -size shape = (fromIntegral $ P.product shape) :: Int +size shape = (fromIntegral $ product shape) :: Int + +-- Todo: get the ident of the dtype from an nd array +indentityElem = undefined +-- Helper for the vectors in identityElem +indentityElem' :: forall a . DType a => Vector a -> a +indentityElem' _ = DType.identity :: DType a => a + +-- | Creates an NdArray from a given shape and list. The number of elements must match. fromList :: DType a => [Integer] -> [a] -> NdArray fromList shape l = if length l /= size shape then error "Length of the list should match the total number of elements specified by the shape." else NdArray shape (V.fromList l) -fromListFlat :: DType a => [Integer] -> [a] -> NdArray -fromList l = NdArray [genericLength @Integer l] (V.fromList l) - -fromMatrix :: TreeMatrix -> NdArray -fromMatrix m = NdArray (matrixShape m) (V.fromList $ flattenToList m) +-- | Creates a 1xn NdArray from a list. +fromListFlat :: DType a => [a] -> NdArray +fromListFlat l = NdArray [fromIntegral$length l :: Integer] (V.fromList l) --- Todo: get the ident of the dtype from an nd array -indentityElem = undefined +{-| Creates an NdArray from an explicitly given matrix such as the example 2x3. -} +-- >>> m = A [A [B 1, B 2], +-- >>> A [B 3, B 4], +-- >>> A [B 5, B 6]] +-- +fromMatrix :: DType a => TreeMatrix a -> NdArray +fromMatrix m = NdArray (matrixShape m) (V.fromList l) + where l = flattenToList $ matrixToTree m -indentityElem' :: forall a . DType a => Vector a -> a -indentityElem' _ = DType.identity :: DType a => a +-- | Creates a 1x1 matrix +singleton :: DType a => a -> NdArray +singleton x = NdArray [1] (V.fromList [x]) +{- | Creates the smallest possible square matrix from the given list, +padding out any required space with the identity element for the DType -} +squareArr = undefined --- Todo: Create ident array of certain shape +{- | Creates an array of the given shape of the identity element for the given type. -} +zeros = undefined {- zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray zeros t s = NdArray zerovec s @@ -145,31 +169,47 @@ expandInd shape i = expandRun shape i 1 expandRun :: [Integer] -> Integer -> Integer -> [Integer] expandRun [] _ _ = [] expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) - where x = (i `P.div` runSize) `P.mod` s + where x = (i `div` runSize) `mod` s -- | Checks an index does not exceed the shape validIndex :: NdArray -> [Integer] -> Bool -validIndex (NdArray s v) i = (length s == length v) && and $ zipWith lessAbs s i - where lessAbs x y = (y>=0 && y [Integer] -> Dtype +-- >>> m = fromListFlat [2,4,8 :: Int] +-- >>> m #! [1] :: Int +-- 4 +-- >>> m #! [50] :: Int +-- 0 +(#!) :: DType a => NdArray -> [Integer] -> a (NdArray s v) #! i = case (NdArray s v) !? i of Just val -> val - Nothing -> indentityElem' v + Nothing -> DType.identity :: DType a => a {- | The safer version of #! which returns Nothing if an index exceeds the shape bounds. -} -(!?) :: NdArray -> [Integer] -> Maybe Dtype +-- >>> m = fromListFlat [2,4,8 :: Int] +-- >>> m !? [1] :: Maybe Int +-- Just 4 +-- >>> m !? [50] :: Maybe Int +-- Nothing +(!?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a (NdArray s v) !? i = let - valid = validIndex (NdArray s v) i + -- Converts any negative indicies to their equivalent positives positives = zipWith (\x y -> if y < 0 then x+y else y) s i + flatInd = fromIntegral $ collapseInd s positives :: Int in - if valid then Just $ v ! (collapseInd s positives) else Nothing + -- The type comparison should always hold + if validIndex (NdArray s v) i then + case ty v `eqTypeRep` typeRep @(Vector a) of + Just HRefl -> Just (v V.! flatInd) :: Maybe a -- Indexing the vector + Nothing -> Nothing + else Nothing -- Todo: slicing @@ -189,7 +229,7 @@ pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then else error $ shapeMismatch (show s) (show r) elemMultiply :: NdArray -> NdArray -> NdArray -elemMultiply = pointwiseZip multiply +elemMultiply = pointwiseZip DType.multiply -- Todo: Needs to operate on doubles --elemDivide :: NdArray -> NdArray -> NdArray @@ -203,7 +243,7 @@ elemDiv = pointwiseZip DType.div --elemPower = pointwiseZip power elemPow :: NdArray -> NdArray -> NdArray -elemPow = pointwiseZip pow +elemPow = pointwiseZip DType.pow -- | Type & Shape Conversion | -- -- Converting between the standard dtypes and changing the shapes of matricies @@ -211,10 +251,13 @@ elemPow = pointwiseZip pow -- To do: add many more possible types you can convert to -- Use the TypeApplications syntax: -- case typeOf x `eqTypeRep` typeRep @Integer of +-- TODO USING dtypetorational +{- matchDType :: NdArray -> NdArray -> Maybe NdArray matchDType (NdArray _ v) (NdArray r u) = case v =@= V.fromList [1::Int] of Just HRefl -> Just $ NdArray r (V.map dtypeToInt u) _ -> Nothing +-} -- Check that the matrix isn't larger than the shape but if so truncate it constrainSize :: DType a => [Integer] -> Vector a -> (Bool, Vector a) @@ -226,7 +269,7 @@ constrainSize s v = -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) padSize :: DType a => [Integer] -> Vector a -> Vector a -padSize s v = v V.++ V.replicate ((size s) - len) identity +padSize s v = v V.++ V.replicate ((size s) - len) DType.identity where len = V.length v -- Contrain or pad the vector to match the size @@ -236,10 +279,10 @@ setSize s v = let (unchanged, u) = constrainSize s v in -- Constrain or pad the NdArray to match the new given size resize :: NdArray -> [Integer] -> NdArray -resize (NdArray _ v) r = NdArray (setSize r v) r +resize (NdArray _ v) r = NdArray r (setSize r v) reshape :: NdArray -> [Integer] -> Maybe NdArray -reshape (NdArray s v) r = if P.product s == P.product r +reshape (NdArray s v) r = if product s == product r then Just $ NdArray r v else Nothing @@ -250,8 +293,8 @@ map1DIndex s r i = collapseInd r (expandInd s i) mapShapeLoss :: [Integer] -> [Integer] -> Bool mapShapeLoss s r = - if P.length r < P.length s then True - else P.or $ P.zipWith (>) s r + if length r < length s then True + else or $ zipWith (>) s r -- If you try to map to a smaller shape, values are dropped & weird stuff happens, otherwise padded @@ -263,12 +306,12 @@ mapShapeLoss s r = padShape :: NdArray -> [Integer] -> NdArray padShape (NdArray s v) r = let - newSize = fromInteger @Int (P.product r) + newSize = fromInteger @Int (product r) nullVec = V.replicate newSize (indentityElem' v) fi i = fromIntegral @Int @Integer i - newIndices = imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v + newIndices = V.imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v in - NdArray (unsafeUpdate_ nullVec newIndices v) r + NdArray r (V.unsafeUpdate_ nullVec newIndices v) -- | Common Errors | -- shapeMismatch :: String -> String -> String From e8087d8bfec9a9659b09cc8f5109c9456922081c Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 20 Jul 2023 15:15:15 +0100 Subject: [PATCH 28/90] pretty printing --- ndarray.cabal => numskull.cabal | 2 +- src/MatrixForm.hs | 22 +++++++++++++++++++++- src/NdArray.hs | 13 +++++++++++++ src/Numskull.hs | 8 +------- src/Serialisation.hs | 1 + 5 files changed, 37 insertions(+), 9 deletions(-) rename ndarray.cabal => numskull.cabal (95%) create mode 100644 src/NdArray.hs diff --git a/ndarray.cabal b/numskull.cabal similarity index 95% rename from ndarray.cabal rename to numskull.cabal index b16774e..96ea3bc 100644 --- a/ndarray.cabal +++ b/numskull.cabal @@ -14,7 +14,7 @@ build-type: Simple library import library-deps - exposed-modules: Numskull, Serialisation, DType + exposed-modules: Numskull, Serialisation, DType, MatrixForm -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index eed50e3..25f746a 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -2,7 +2,9 @@ module MatrixForm where +import NdArray import Data.Tree +import qualified Data.Vector.Storable as V data TreeMatrix a = B a | A [TreeMatrix a] @@ -33,4 +35,22 @@ treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLe where levelLen = map length $ levels t matrixShape :: TreeMatrix a -> [Integer] -matrixShape = treeShape . matrixToTree \ No newline at end of file +matrixShape = treeShape . matrixToTree + + +addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] +addNewlines [] xs = xs +addNewlines (l:ls) xs = map (\(i,x) -> if i /= 0 && i `mod` l == 0 then (i, "\n"++x) else (i,x)) (addNewlines ls xs) + +padStringTo :: Int -> String -> String +padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " + +printArray :: NdArray -> IO () +printArray (NdArray s v) = putStr $ conc <> "\n" + where + vl = map show (V.toList v) + largest = maximum $ map length vl + newlines = scanl1 (*) s + spaced = zipWith (\i x -> (i, padStringTo largest x)) [0..] vl + lined = addNewlines newlines spaced + conc = concatMap snd lined diff --git a/src/NdArray.hs b/src/NdArray.hs new file mode 100644 index 0000000..b6d8360 --- /dev/null +++ b/src/NdArray.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE GADTs #-} + +module NdArray where + +import DType +import Data.Vector.Storable + +-- * NdArray +-- Todo: Should shapes be [Integer] or [Int] or maybe even another vector? +-- | The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are +-- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). +data NdArray where + NdArray :: DType a => [Integer] -> Vector a -> NdArray \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index 432fef1..4e0de71 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -6,6 +6,7 @@ module Numskull where +import NdArray import qualified DType import DType (DType) import MatrixForm @@ -31,13 +32,6 @@ ty = typeOf (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) --- * NdArray --- Todo: Should shapes be [Integer] or [Int] or maybe even another vector? --- | The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are --- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). -data NdArray where - NdArray :: DType a => [Integer] -> Vector a -> NdArray - -- Todo: show in a nicer shapely form :) instance Show NdArray where show (NdArray s v) = show s <> " " <> show v diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 44d00a2..e90dd48 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -2,6 +2,7 @@ module Serialisation where import Numskull import DType +import NdArray import System.IO import Data.List as List From 278700a446c9edbe68dfa5eb684c3cb1925d7842 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 20 Jul 2023 17:08:12 +0100 Subject: [PATCH 29/90] Much numskull documentation --- src/MatrixForm.hs | 20 +++---- src/Numskull.hs | 130 ++++++++++++++++++++++++++++++---------------- 2 files changed, 97 insertions(+), 53 deletions(-) diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index 25f746a..a53e9f3 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -8,12 +8,14 @@ import qualified Data.Vector.Storable as V data TreeMatrix a = B a | A [TreeMatrix a] +-- READING MATRICIES matrixToTree :: TreeMatrix a -> Tree [a] matrixToTree (B x) = Node [x] [] matrixToTree (A xs) = Node [] (map matrixToTree xs) -- Example 2x3x2 {- +l :: TreeMatrix Int l = A [A [A [B 1, B 2], A [B 3, B 4], A [B 5, B 6]], @@ -35,16 +37,9 @@ treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLe where levelLen = map length $ levels t matrixShape :: TreeMatrix a -> [Integer] -matrixShape = treeShape . matrixToTree - - -addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] -addNewlines [] xs = xs -addNewlines (l:ls) xs = map (\(i,x) -> if i /= 0 && i `mod` l == 0 then (i, "\n"++x) else (i,x)) (addNewlines ls xs) - -padStringTo :: Int -> String -> String -padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " +matrixShape = reverse . treeShape . matrixToTree +-- WRITING MATRICIES printArray :: NdArray -> IO () printArray (NdArray s v) = putStr $ conc <> "\n" where @@ -54,3 +49,10 @@ printArray (NdArray s v) = putStr $ conc <> "\n" spaced = zipWith (\i x -> (i, padStringTo largest x)) [0..] vl lined = addNewlines newlines spaced conc = concatMap snd lined + +padStringTo :: Int -> String -> String +padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " + +addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] +addNewlines [] xs = xs +addNewlines (l:ls) xs = map (\(i,x) -> if i /= 0 && i `mod` l == 0 then (i, "\n"++x) else (i,x)) (addNewlines ls xs) \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index 4e0de71..ea05eac 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -17,6 +17,7 @@ import Type.Reflection -- $setup -- >>> import Numskull as N +-- >>> import qualified Vector -- * Typing Shorthand @@ -95,25 +96,36 @@ indentityElem' :: forall a . DType a => Vector a -> a indentityElem' _ = DType.identity :: DType a => a -- | Creates an NdArray from a given shape and list. The number of elements must match. +-- >>> printArray $ fromList [2,2] [1,2,3,4::Int] +-- 1 2 +-- 3 4 fromList :: DType a => [Integer] -> [a] -> NdArray fromList shape l = if length l /= size shape then error "Length of the list should match the total number of elements specified by the shape." else NdArray shape (V.fromList l) -- | Creates a 1xn NdArray from a list. +-- >>> printArray $ fromListFlat [1,2,3,4::Int] +-- 1 2 3 4 fromListFlat :: DType a => [a] -> NdArray -fromListFlat l = NdArray [fromIntegral$length l :: Integer] (V.fromList l) +fromListFlat l = NdArray [toInteger$length l] (V.fromList l) {-| Creates an NdArray from an explicitly given matrix such as the example 2x3. -} +-- >>> m :: TreeMatrix Int -- >>> m = A [A [B 1, B 2], -- >>> A [B 3, B 4], -- >>> A [B 5, B 6]] --- +-- >>> printArray $ fromMatrix m +-- 1 2 +-- 3 4 +-- 5 6 fromMatrix :: DType a => TreeMatrix a -> NdArray fromMatrix m = NdArray (matrixShape m) (V.fromList l) where l = flattenToList $ matrixToTree m -- | Creates a 1x1 matrix +-- >>> printArray $ singleton (3::Int) +-- 3 singleton :: DType a => a -> NdArray singleton x = NdArray [1] (V.fromList [x]) @@ -165,6 +177,10 @@ expandRun [] _ _ = [] expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) where x = (i `div` runSize) `mod` s +-- | Converts the multi-index for one shape to another +map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer +map1DIndex s r i = collapseInd r (expandInd s i) + -- | Checks an index does not exceed the shape validIndex :: NdArray -> [Integer] -> Bool validIndex (NdArray s _) i = (length i == length s) && (and $ zipWith lessAbs i s) @@ -207,14 +223,19 @@ value for the array e.g. 0. To avoid this use !?. -- Todo: slicing --- | Pointwise Functions | -- +-- * Pointwise Functions -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances -- Single Argument -- To do ;) -- Two Arguments --- https://hackage.haskell.org/package/base-4.18.0.0/docs/Control-Monad-ST.html#v:runST +-- | The generic function for operating on two DType arrays with the same shape in an element-wise/pointwise way. +-- >>> x = fromList [2,2] [1,2,3,4 :: Int] +-- >>> y = fromList [2,2] [5,2,2,2 :: Int] +-- >>> printArray $ pointwiseZip (DType.multiply) x y +-- 5 4 +-- 6 8 pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of @@ -222,6 +243,7 @@ pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then Nothing -> error $ typeMismatch (show$ty v) (show$ty u) else error $ shapeMismatch (show s) (show r) +-- | Pointwise multiplication elemMultiply :: NdArray -> NdArray -> NdArray elemMultiply = pointwiseZip DType.multiply @@ -229,6 +251,7 @@ elemMultiply = pointwiseZip DType.multiply --elemDivide :: NdArray -> NdArray -> NdArray --elemDivide = pointwiseZip divide +-- | Pointwise integer division elemDiv :: NdArray -> NdArray -> NdArray elemDiv = pointwiseZip DType.div @@ -236,11 +259,16 @@ elemDiv = pointwiseZip DType.div --elemPower :: NdArray -> NdArray -> NdArray --elemPower = pointwiseZip power +-- | Pointwise exponentiation elemPow :: NdArray -> NdArray -> NdArray elemPow = pointwiseZip DType.pow --- | Type & Shape Conversion | -- --- Converting between the standard dtypes and changing the shapes of matricies +-- * Type & Shape Conversion +{- | Converting between the standard dtypes and changing the shapes of arrays. +NB the difference between 'size' and 'shape'. The shape is an Integer list +describing the width of each dimension. Size refers to the total number of +elements in the array, i.e. the product of the shape. +-} -- To do: add many more possible types you can convert to -- Use the TypeApplications syntax: @@ -253,61 +281,75 @@ matchDType (NdArray _ v) (NdArray r u) = case v =@= V.fromList [1::Int] of _ -> Nothing -} --- Check that the matrix isn't larger than the shape but if so truncate it -constrainSize :: DType a => [Integer] -> Vector a -> (Bool, Vector a) +{- Helper which checks that the array isn't larger than the shape contraints. +If it is valid the Boolean in the pair will be true and the vector is returned. +If it is invalid the vector is truncated first. +-} +constrainSize :: DType a => Integer -> Vector a -> (Bool, Vector a) constrainSize s v = - if (size s) < len then (False, V.take (size s) v) + if si < V.length v then (False, V.take si v) else (True, v) where - len = V.length v + si = fromIntegral s :: Int -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) -padSize :: DType a => [Integer] -> Vector a -> Vector a -padSize s v = v V.++ V.replicate ((size s) - len) DType.identity +padSize :: DType a => Integer -> Vector a -> Vector a +padSize s v = v V.++ V.replicate ((fromIntegral s ::Int) - len) DType.identity where len = V.length v -- Contrain or pad the vector to match the size -setSize :: DType a => [Integer] -> Vector a -> Vector a +setSize :: DType a => Integer -> Vector a -> Vector a setSize s v = let (unchanged, u) = constrainSize s v in if unchanged then padSize s u else u --- Constrain or pad the NdArray to match the new given size -resize :: NdArray -> [Integer] -> NdArray -resize (NdArray _ v) r = NdArray r (setSize r v) - -reshape :: NdArray -> [Integer] -> Maybe NdArray -reshape (NdArray s v) r = if product s == product r +{- | Truncate or pad the NdArray to match the new given size. +The shape will be collapsed to 1xn. +-} +-- >>> x = fromList [2,2] [1,2,3,4 :: Int] +-- >>> printArray $ resize 6 x +-- 1 2 3 4 0 0 +-- >>> printArray $ resize 2 x +-- 1 2 +resize :: Integer -> NdArray -> NdArray +resize s (NdArray _ v) = NdArray [s] (setSize s v) + +-- | Shape-shift one array to another of the same size (Nothing otherwise). +-- >>> x = fromList [2,3] [1,2,3,4,5,6 :: Int] +-- >>> printArray x +-- 1 2 +-- 3 4 +-- 5 6 +-- >>> printArray $ fromJust $ reshape [3,2] x +-- 1 2 3 +-- 4 5 6 +reshape :: [Integer] -> NdArray -> Maybe NdArray +reshape r (NdArray s v) = if product s == product r then Just $ NdArray r v else Nothing ---NB: reshape will pad/truncate individual dimensions whereas resize keeps as many values as possible but they might switch position --- a matrix being reshaped must already match the size correctly -map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer -map1DIndex s r i = collapseInd r (expandInd s i) - -mapShapeLoss :: [Integer] -> [Integer] -> Bool -mapShapeLoss s r = - if length r < length s then True - else or $ zipWith (>) s r - --- If you try to map to a smaller shape, values are dropped & weird stuff happens, otherwise padded - --- | Some helpful documentation --- --- >>> padShape (Numskull.fromList [2] [3,4 :: Int]) [5] --- [3,4,0,0,0][5] --- -padShape :: NdArray -> [Integer] -> NdArray -padShape (NdArray s v) r = +-- Checks that the first shape is smaller or equal to the second. +smallerShape :: [Integer] -> [Integer] -> Bool +smallerShape s r = + if length s > length s then False + else and $ zipWith (<=) s r + +-- | Adds zero-rows to an array. Will error if you map to a smaller shape. +-- >>> x = fromList [2,2] [1,2,3,4 :: Int] +-- >>> printArray $ padShape [4,3] x +-- 1 2 0 0 +-- 3 4 0 0 +-- 0 0 0 0 +padShape :: [Integer] -> NdArray -> NdArray +padShape r (NdArray s v) = let - newSize = fromInteger @Int (product r) - nullVec = V.replicate newSize (indentityElem' v) - fi i = fromIntegral @Int @Integer i - newIndices = V.imap (\i _ -> fromInteger @Int $ map1DIndex s r (fi i)) v + nullVec = V.replicate (size r) (indentityElem' v) + newIndices = V.imap (\i _ -> fromIntegral $ map1DIndex s r (toInteger i) :: Int) v in - NdArray r (V.unsafeUpdate_ nullVec newIndices v) + if smallerShape s r + then NdArray r (V.unsafeUpdate_ nullVec newIndices v) + else error "Cannot map to a smaller shape." --- | Common Errors | -- +-- * Common Errors shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." From 1d4fd01f1b1586247d5bf21734cd6c37df3f9558 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 21 Jul 2023 17:35:47 +0100 Subject: [PATCH 30/90] working on matrix multiplication --- src/Numskull.hs | 102 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index ea05eac..53f1fb7 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -14,6 +14,8 @@ import MatrixForm import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import Type.Reflection +import qualified Data.Map as M +import Data.Maybe (fromJust) -- $setup -- >>> import Numskull as N @@ -86,7 +88,10 @@ instance Num NdArray where -- >>> size [2,3] -- 6 size :: [Integer] -> Int -size shape = (fromIntegral $ product shape) :: Int +size sh = (fromIntegral $ product sh) :: Int + +shape :: NdArray -> [Integer] +shape (NdArray s _) = s -- Todo: get the ident of the dtype from an nd array indentityElem = undefined @@ -100,9 +105,9 @@ indentityElem' _ = DType.identity :: DType a => a -- 1 2 -- 3 4 fromList :: DType a => [Integer] -> [a] -> NdArray -fromList shape l = - if length l /= size shape then error "Length of the list should match the total number of elements specified by the shape." - else NdArray shape (V.fromList l) +fromList sh l = + if length l /= size sh then error "Length of the list should match the total number of elements specified by the shape." + else NdArray sh (V.fromList l) -- | Creates a 1xn NdArray from a list. -- >>> printArray $ fromListFlat [1,2,3,4::Int] @@ -129,19 +134,19 @@ fromMatrix m = NdArray (matrixShape m) (V.fromList l) singleton :: DType a => a -> NdArray singleton x = NdArray [1] (V.fromList [x]) +arange = undefined + {- | Creates the smallest possible square matrix from the given list, padding out any required space with the identity element for the DType -} squareArr = undefined {- | Creates an array of the given shape of the identity element for the given type. -} -zeros = undefined -{- zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray -zeros t s = NdArray zerovec s +zeros t s = NdArray s zerovec where ident = (DType.identity :: DType a => a) zerovec = (V.replicate (size s) ident) :: DType a => Vector a --} + -- * Indexing & Slicing {- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, @@ -156,10 +161,24 @@ shape of the array, [sx,sy,sz,...], as follows: ... -} +generateIndicies :: [Integer] -> [[Integer]] +generateIndicies = map reverse . foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] +-- foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] [2,3,2] + +mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) +mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) + where + twoDinds = generateIndicies sh + oneDkey = zip [0..] twoDinds + twoDkey = zip twoDinds [0..] + +-- trying to put the type EVERYWHERE haha +vecInd :: forall a . DType a => M.Map [Integer] Int -> (forall a . DType a => NdArray) -> [Integer] -> a +vecInd mapp (NdArray _ (v :: forall a . DType a => Vector a)) i = v V.! (mapp M.! i) -- | Converts a shape and multi-index to a 1D index. collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd shape indicies = collapseRun shape indicies 1 +collapseInd sh indicies = collapseRun sh indicies 1 -- Helper for collapseInd collapseRun :: [Integer] -> [Integer] -> Integer -> Integer @@ -169,7 +188,7 @@ collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) -- | Converts a shape and 1D index to a multi-index. expandInd :: [Integer] -> Integer -> [Integer] -expandInd shape i = expandRun shape i 1 +expandInd sh i = expandRun sh i 1 -- Helper for expandInd expandRun :: [Integer] -> Integer -> Integer -> [Integer] @@ -225,11 +244,15 @@ value for the array e.g. 0. To avoid this use !?. -- * Pointwise Functions -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances --- Single Argument --- To do ;) +----- One Argument + +mapA :: (DType a, DType b) => (a -> b) -> NdArray -> NdArray +--map f (NdArray s v) = NdArray s (fmap f v) +mapA = undefined + +----- Two Arguments --- Two Arguments -- | The generic function for operating on two DType arrays with the same shape in an element-wise/pointwise way. -- >>> x = fromList [2,2] [1,2,3,4 :: Int] -- >>> y = fromList [2,2] [5,2,2,2 :: Int] @@ -270,6 +293,14 @@ describing the width of each dimension. Size refers to the total number of elements in the array, i.e. the product of the shape. -} +-- | Converts an NdArray of one type to any other with a DType instance. +convertDTypeTo :: DType a => NdArray -> TypeRep a -> NdArray +convertDTypeTo = undefined + +-- | Converts the second NdArray to be the same DType as the first. +matchDType :: NdArray -> NdArray -> NdArray +matchDType = undefined + -- To do: add many more possible types you can convert to -- Use the TypeApplications syntax: -- case typeOf x `eqTypeRep` typeRep @Integer of @@ -349,9 +380,54 @@ padShape r (NdArray s v) = then NdArray r (V.unsafeUpdate_ nullVec newIndices v) else error "Cannot map to a smaller shape." +-- * Matrix Operations + +-- For now, just nxm and mxp = nxp +{- +matMul :: NdArray -> NdArray -> NdArray +matMul (NdArray s v) (NdArray r u) = + if (length s /= 2) || (length r /= 2) || s!!1 /= r!!0 then + error "Invalid matrix dimensions." + else case v =@= u of + Nothing -> error "Mismatching types" + Just HRefl -> + + NdArray sh (matMulVec sh (NdArray s v) (NdArray r u)) + where sh = [s!!0, r!!1] + +-- returning the vector result of the matMul +matMulVec :: DType a => + [Integer] -> NdArray -> NdArray -> Vector a + +matMulVec sh nd1 nd2 = + let + (oneDkey, twoDkey) = mapIndicies sh + sz = M.size oneDkey + in + V.generate sz (matMulElem twoDkey nd1 nd2 . (M.!) oneDkey) + +-- element at position [i,j] in the resultant nxp matrix (from matMultiplying a nxm and mxp) +matMulElem :: DType a => + M.Map [Integer] Int -> NdArray -> NdArray -> [Integer] -> a + +matMulElem mapp nd1 nd2 (i:j:_) = + let + (>!) = vecInd mapp -- Map the 2D index to 1D & get value + ks = [1 .. shape nd2 !! 0] + z = DType.identity + in + foldr (\k acc -> DType.add acc $ DType.multiply (nd1>![i,k]) (nd2>![k,j])) z ks + --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] +-} + -- * Common Errors shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." typeMismatch :: String -> String -> String typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array of type '" <> t2 <> "'." + +nd1 :: NdArray +nd1 = fromList [3,2] [1,2,3,4,5,6::Int] +nd2 :: NdArray +nd2 = fromList [2,3] [0,2,4,6,8,10::Int] From fa928ba7e74002601b0dbc6b2ad22fcbf96e725f Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 24 Jul 2023 15:30:18 +0100 Subject: [PATCH 31/90] matrix multiplication --- src/Serialisation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serialisation.hs b/src/Serialisation.hs index e90dd48..827a98b 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -106,6 +106,6 @@ testsave :: IO () testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (S.fromList [1,2,3 :: Int]) ) testload :: IO () -testload = do +testload = do nd <- loadNpy "./src/testout/test123.npy" putStrLn $ show $ nd \ No newline at end of file From 3b8d00591d1e1b913d26ca4a9a2d4cf59f8bc1d6 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 24 Jul 2023 15:30:33 +0100 Subject: [PATCH 32/90] matrix multiplication --- src/MatrixForm.hs | 4 +-- src/Numskull.hs | 79 ++++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index a53e9f3..98b39f8 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -37,7 +37,7 @@ treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLe where levelLen = map length $ levels t matrixShape :: TreeMatrix a -> [Integer] -matrixShape = reverse . treeShape . matrixToTree +matrixShape = treeShape . matrixToTree -- WRITING MATRICIES printArray :: NdArray -> IO () @@ -45,7 +45,7 @@ printArray (NdArray s v) = putStr $ conc <> "\n" where vl = map show (V.toList v) largest = maximum $ map length vl - newlines = scanl1 (*) s + newlines = scanr1 (*) s spaced = zipWith (\i x -> (i, padStringTo largest x)) [0..] vl lined = addNewlines newlines spaced conc = concatMap snd lined diff --git a/src/Numskull.hs b/src/Numskull.hs index 53f1fb7..57853d0 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -3,6 +3,7 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE FlexibleContexts #-} module Numskull where @@ -142,7 +143,7 @@ squareArr = undefined {- | Creates an array of the given shape of the identity element for the given type. -} zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray -zeros t s = NdArray s zerovec +zeros _ s = NdArray s zerovec where ident = (DType.identity :: DType a => a) zerovec = (V.replicate (size s) ident) :: DType a => Vector a @@ -162,7 +163,7 @@ shape of the array, [sx,sy,sz,...], as follows: -} generateIndicies :: [Integer] -> [[Integer]] -generateIndicies = map reverse . foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] +generateIndicies = foldr (\x xs -> [ (i:t) | i <- [0..(x-1)], t <- xs]) [[]] -- foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] [2,3,2] mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) @@ -172,13 +173,15 @@ mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) oneDkey = zip [0..] twoDinds twoDkey = zip twoDinds [0..] --- trying to put the type EVERYWHERE haha -vecInd :: forall a . DType a => M.Map [Integer] Int -> (forall a . DType a => NdArray) -> [Integer] -> a -vecInd mapp (NdArray _ (v :: forall a . DType a => Vector a)) i = v V.! (mapp M.! i) +-- Unsafe: Indexes the vector with the multi-index using a mapping +vecInd :: forall a . DType a => M.Map [Integer] Int -> Vector a -> [Integer] -> a +vecInd mapp v i = v V.! (mapp M.! i) +--vecInd mapp v i = case v =@= (undefined :: Vector a) of +-- Just HRefl -> v V.! (mapp M.! i) -- | Converts a shape and multi-index to a 1D index. collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd sh indicies = collapseRun sh indicies 1 +collapseInd sh indicies = collapseRun (reverse$sh) (reverse$indicies) 1 -- Helper for collapseInd collapseRun :: [Integer] -> [Integer] -> Integer -> Integer @@ -188,7 +191,7 @@ collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) -- | Converts a shape and 1D index to a multi-index. expandInd :: [Integer] -> Integer -> [Integer] -expandInd sh i = expandRun sh i 1 +expandInd sh i = reverse $ expandRun (reverse$sh) i 1 -- Helper for expandInd expandRun :: [Integer] -> Integer -> Integer -> [Integer] @@ -383,42 +386,38 @@ padShape r (NdArray s v) = -- * Matrix Operations -- For now, just nxm and mxp = nxp -{- matMul :: NdArray -> NdArray -> NdArray -matMul (NdArray s v) (NdArray r u) = +matMul (NdArray s v) (NdArray r u) = if (length s /= 2) || (length r /= 2) || s!!1 /= r!!0 then error "Invalid matrix dimensions." - else case v =@= u of - Nothing -> error "Mismatching types" - Just HRefl -> - - NdArray sh (matMulVec sh (NdArray s v) (NdArray r u)) - where sh = [s!!0, r!!1] + else case v =@= u of + Just HRefl -> NdArray sh (matMulVec s v r u) + _ -> error "Mismatching types" + where + sh = [s!!0, r!!1] -- returning the vector result of the matMul -matMulVec :: DType a => - [Integer] -> NdArray -> NdArray -> Vector a - -matMulVec sh nd1 nd2 = - let - (oneDkey, twoDkey) = mapIndicies sh - sz = M.size oneDkey - in - V.generate sz (matMulElem twoDkey nd1 nd2 . (M.!) oneDkey) - --- element at position [i,j] in the resultant nxp matrix (from matMultiplying a nxm and mxp) -matMulElem :: DType a => - M.Map [Integer] Int -> NdArray -> NdArray -> [Integer] -> a - -matMulElem mapp nd1 nd2 (i:j:_) = +matMulVec :: forall a . DType a => + [Integer] -> Vector a -> [Integer] -> Vector a -> Vector a +matMulVec s v r u = let - (>!) = vecInd mapp -- Map the 2D index to 1D & get value - ks = [1 .. shape nd2 !! 0] - z = DType.identity + oneDkey = fst $ mapIndicies [s!!0, r!!1] + sz = M.size oneDkey + map1 = vecInd (snd $ mapIndicies s) v + map2 = vecInd (snd $ mapIndicies r) u + ks = [0 .. (s!!1 -1)] in - foldr (\k acc -> DType.add acc $ DType.multiply (nd1>![i,k]) (nd2>![k,j])) z ks + V.generate sz (matMulElem map1 map2 ks . (M.!) oneDkey) + +-- element at position [i,j] in the resultant nxp matrix (from matMultiplying a prev: mxn and pxm = pxn) +--matMulElem :: DType a => +-- NdArray -> M.Map [Integer] Int -> NdArray -> M.Map [Integer] Int -> [Integer] -> a +matMulElem :: DType a => + ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a +matMulElem map1 map2 ks (i:j:_) = + foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.identity ks --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] --} + -- * Common Errors shapeMismatch :: String -> String -> String @@ -427,7 +426,9 @@ shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with arr typeMismatch :: String -> String -> String typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array of type '" <> t2 <> "'." -nd1 :: NdArray -nd1 = fromList [3,2] [1,2,3,4,5,6::Int] -nd2 :: NdArray -nd2 = fromList [2,3] [0,2,4,6,8,10::Int] +ndt1 :: NdArray +ndt1 = fromList [3,2] [1,2,3,4,5,6::Int] +ndt2 :: NdArray +ndt2 = fromList [2,3] [0,2,4,6,8,10::Int] + +nd3 = fromList [2,2] [1,2,3,4 :: Int] From ff951ffac64b5efa007a1bd082c9547b712a5753 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 24 Jul 2023 17:43:47 +0100 Subject: [PATCH 33/90] working on conversions --- src/Numskull.hs | 72 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 57853d0..4aa1f6f 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -250,9 +250,49 @@ value for the array e.g. 0. To avoid this use !?. ----- One Argument -mapA :: (DType a, DType b) => (a -> b) -> NdArray -> NdArray ---map f (NdArray s v) = NdArray s (fmap f v) -mapA = undefined +mapA :: forall a . forall b . (DType a, DType b) => (a -> b) -> NdArray -> NdArray +mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of + Just HRefl -> NdArray s (V.map f v) + _ -> error "Function input does not match array type." + +--mapA :: (forall a . forall b . (DType a, DType b) => a -> b) -> NdArray -> NdArray +--mapA f (NdArray s v) = NdArray s (V.map f v) + +mapTransform :: (forall a . DType a => a -> a) -> NdArray -> NdArray +mapTransform f (NdArray s v) = NdArray s (V.map f v) + +scale :: forall a . DType a => a -> NdArray -> NdArray +scale x = mapA (DType.multiply x) + +abs :: NdArray -> NdArray +abs = mapTransform (DType.abs) + +signum :: NdArray -> NdArray +signum = mapTransform (DType.signum) + +ceil :: NdArray -> NdArray +ceil = mapTransform (DType.ceil) + +floor :: NdArray -> NdArray +floor = mapTransform (DType.floor) + +sin :: NdArray -> NdArray +sin = mapTransform (DType.sin) + +cos :: NdArray -> NdArray +cos = mapTransform (DType.cos) + +tan :: NdArray -> NdArray +tan = mapTransform (DType.tan) + +invert :: NdArray -> NdArray +invert = mapTransform (DType.invert) + +shiftleft :: NdArray -> NdArray +shiftleft = mapTransform (DType.shiftleft) + +shiftright :: NdArray -> NdArray +shiftright = mapTransform (DType.shiftright) ----- Two Arguments @@ -295,14 +335,34 @@ NB the difference between 'size' and 'shape'. The shape is an Integer list describing the width of each dimension. Size refers to the total number of elements in the array, i.e. the product of the shape. -} +extractType' :: forall a . DType a => Vector a -> TypeRep a +extractType' v = typeRep @a +extractType :: DType a => NdArray -> TypeRep a +extractType (NdArray s v) = extractType' v -- | Converts an NdArray of one type to any other with a DType instance. -convertDTypeTo :: DType a => NdArray -> TypeRep a -> NdArray -convertDTypeTo = undefined +convertDTypeTo :: forall a . DType a => TypeRep a -> NdArray -> NdArray +convertDTypeTo t (NdArray s v) = NdArray s (V.map convert v) + where + convert x = case (V.singleton x) =@= v of + Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a + _ -> error "Impossible type mismatch." -- | Converts the second NdArray to be the same DType as the first. matchDType :: NdArray -> NdArray -> NdArray -matchDType = undefined +matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) + where + convert x = case (V.singleton x) =@= u of + Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a + _ -> error "Impossible type mismatch." + +-- Todo extract the type then call this fnction to effectively pattern match on the type +matchDType' :: NdArray -> TypeRep a -> NdArray -> TypeRep b -> NdArray +matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) + where + convert x = case (V.singleton x) =@= u of + Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a + _ -> error "Impossible type mismatch." -- To do: add many more possible types you can convert to -- Use the TypeApplications syntax: From 443a938cc0bf18f386e44c6a0d4c8a0e308ad475 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 25 Jul 2023 10:22:19 +0100 Subject: [PATCH 34/90] Dtype conversions done --- src/Numskull.hs | 52 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 4aa1f6f..8da9628 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -94,6 +94,16 @@ size sh = (fromIntegral $ product sh) :: Int shape :: NdArray -> [Integer] shape (NdArray s _) = s +-- | Gets the TypeRep for the NdArray elements +ndType :: forall a . DType a => NdArray -> TypeRep a +ndType (NdArray _ v) = case v =@= (undefined :: Vector a) of + Just HRefl -> vecType v :: TypeRep a + _ -> error "Impossible type mismatch." + +-- Helper to get the vector typeRep +vecType :: forall a . DType a => Vector a -> TypeRep a +vecType _ = typeRep @a + -- Todo: get the ident of the dtype from an nd array indentityElem = undefined @@ -335,19 +345,26 @@ NB the difference between 'size' and 'shape'. The shape is an Integer list describing the width of each dimension. Size refers to the total number of elements in the array, i.e. the product of the shape. -} -extractType' :: forall a . DType a => Vector a -> TypeRep a -extractType' v = typeRep @a -extractType :: DType a => NdArray -> TypeRep a -extractType (NdArray s v) = extractType' v -- | Converts an NdArray of one type to any other with a DType instance. convertDTypeTo :: forall a . DType a => TypeRep a -> NdArray -> NdArray -convertDTypeTo t (NdArray s v) = NdArray s (V.map convert v) - where - convert x = case (V.singleton x) =@= v of - Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a - _ -> error "Impossible type mismatch." +convertDTypeTo t (NdArray s v) = convertDTFromTo (vecType v) t (NdArray s v) + +-- Helper with additional typing information +convertDTFromTo :: forall a b . (DType a, DType b) => + TypeRep a -> TypeRep b -> NdArray -> NdArray +convertDTFromTo t1 t2 (NdArray s v) = case v =@= (undefined :: Vector a) of + Just HRefl -> NdArray s (V.map convert v) + _ -> error "Impossible type mismatch." + where + convert :: (DType a, DType b) => a -> b + convert x = DType.rationalToDtype (DType.dtypeToRational x) +-- | Converts the second NdArray to be the same DType as the first. +matchDType :: NdArray -> NdArray -> NdArray +matchDType (NdArray _ v) nd = convertDTypeTo (vecType v) nd + +{- -- | Converts the second NdArray to be the same DType as the first. matchDType :: NdArray -> NdArray -> NdArray matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) @@ -355,14 +372,19 @@ matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) convert x = case (V.singleton x) =@= u of Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a _ -> error "Impossible type mismatch." - +-} +{- -- Todo extract the type then call this fnction to effectively pattern match on the type -matchDType' :: NdArray -> TypeRep a -> NdArray -> TypeRep b -> NdArray -matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) - where - convert x = case (V.singleton x) =@= u of - Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a +matchDType' :: forall a . forall b . NdArray -> TypeRep a -> NdArray -> TypeRep b -> NdArray +matchDType' (NdArray _ v) t1 (NdArray r u) t2 = NdArray r (V.map convert u) + where + convert x = case eqTypeRep (ty v) t1 of + Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: b _ -> error "Impossible type mismatch." +-} + --case x =@= (undefined :: a) of + -- Just HRefl -> + -- _ -> error "Impossible type mismatch." -- To do: add many more possible types you can convert to -- Use the TypeApplications syntax: From face528bc14f1235609d9d29ed17de22dd78eeca Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 25 Jul 2023 15:44:18 +0100 Subject: [PATCH 35/90] serialisation generalisation progess --- src/Numskull.hs | 42 +---------- src/Serialisation.hs | 164 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 135 insertions(+), 71 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 8da9628..28950d6 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -65,15 +65,14 @@ instance Ord NdArray where Nothing -> error $ typeMismatch (show v) (show u) else error $ shapeMismatch (show s) (show r) --- To do: matrix multiplication :O -- To do: change fromInteger to return an integer array rather than int instance Num NdArray where -- | Adds elements pointwise (+) = pointwiseZip DType.add -- | Subtracts elements pointwise (-) = pointwiseZip DType.subtract - -- | Matrix multiplication TODO - (*) = undefined + -- | Multiplies elements pointwise + (*) = pointwiseZip DType.multiply -- | Inverts all elements according to their DType instance negate (NdArray s v) = NdArray s (V.map DType.invert v) -- | Absolute value of each element @@ -319,10 +318,6 @@ pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then Nothing -> error $ typeMismatch (show$ty v) (show$ty u) else error $ shapeMismatch (show s) (show r) --- | Pointwise multiplication -elemMultiply :: NdArray -> NdArray -> NdArray -elemMultiply = pointwiseZip DType.multiply - -- Todo: Needs to operate on doubles --elemDivide :: NdArray -> NdArray -> NdArray --elemDivide = pointwiseZip divide @@ -364,39 +359,6 @@ convertDTFromTo t1 t2 (NdArray s v) = case v =@= (undefined :: Vector a) of matchDType :: NdArray -> NdArray -> NdArray matchDType (NdArray _ v) nd = convertDTypeTo (vecType v) nd -{- --- | Converts the second NdArray to be the same DType as the first. -matchDType :: NdArray -> NdArray -> NdArray -matchDType (NdArray _ v) (NdArray r u) = NdArray r (V.map convert u) - where - convert x = case (V.singleton x) =@= u of - Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: a - _ -> error "Impossible type mismatch." --} -{- --- Todo extract the type then call this fnction to effectively pattern match on the type -matchDType' :: forall a . forall b . NdArray -> TypeRep a -> NdArray -> TypeRep b -> NdArray -matchDType' (NdArray _ v) t1 (NdArray r u) t2 = NdArray r (V.map convert u) - where - convert x = case eqTypeRep (ty v) t1 of - Just HRefl -> DType.rationalToDtype (DType.dtypeToRational x) :: b - _ -> error "Impossible type mismatch." --} - --case x =@= (undefined :: a) of - -- Just HRefl -> - -- _ -> error "Impossible type mismatch." - --- To do: add many more possible types you can convert to --- Use the TypeApplications syntax: --- case typeOf x `eqTypeRep` typeRep @Integer of --- TODO USING dtypetorational -{- -matchDType :: NdArray -> NdArray -> Maybe NdArray -matchDType (NdArray _ v) (NdArray r u) = case v =@= V.fromList [1::Int] of - Just HRefl -> Just $ NdArray r (V.map dtypeToInt u) - _ -> Nothing --} - {- Helper which checks that the array isn't larger than the shape contraints. If it is valid the Boolean in the pair will be true and the vector is returned. If it is invalid the vector is truncated first. diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 827a98b..48fe818 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE TypeApplications #-} + module Serialisation where import Numskull @@ -7,14 +10,16 @@ import NdArray import System.IO import Data.List as List import Data.List.Split -import qualified Data.Vector.Storable as S +import qualified Data.Vector.Storable as V +import qualified Data.Map as M +import Type.Reflection import Foreign (Ptr, alloca, mallocBytes) import Foreign.Storable (poke, peek, sizeOf) import Data.Word (Word16) -- | HASKELL TO PYTHON | -- --- Built in numpy serialisation descriptions +-- | Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String getNumpyDType (NdArray _ v) = case show $ ty v of "Vector Int" -> " " error "Non-standard types cannot be serialised. Yet." --- Converts shape list to a string of the Numpy tuple form e.g. (3,2,) +-- | Converts shape list to a string of the Numpy tuple form e.g. (3,2,) getNumpyShape :: NdArray -> String getNumpyShape (NdArray s _) = "(" <> (drop 1 $ take (length lshape -1) $ lshape) <> ",)" where lshape = show s --- Maximum memory required for any single element in the array +-- | Gets the maximum memory required for any single element in an array getElemSize :: NdArray -> Int -getElemSize (NdArray _ v) = S.maximum $ S.map sizeOf v +getElemSize (NdArray _ v) = V.maximum $ V.map sizeOf v --- Saves any of the standard types defined above as a .npy +-- | Saves any of the standard types defined above as a .npy -- Thanks Chris! https://github.com/cchalmers/dense/blob/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262/src/Data/Dense/Storable.hs#L686 --- see https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html +-- See https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html saveNpy :: FilePath -> NdArray -> IO () saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do let @@ -64,16 +69,102 @@ saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do hPutStr h (List.replicate padding ' ') hPutChar h '\n' -- Put vector body - S.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) + V.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) + -- | PYTHON TO HASKELL | -- +-- METADATA +listDict :: String -> [String] +listDict x = splitOn " " $ (splitOneOf "{}" (filter (/='\'') x)) !! 1 + +pairDict :: [String] -> [(String, String)] +pairDict [] = [] +pairDict (_:[]) = [] +pairDict (k:v:ls) = (k, v) : pairDict ls + +-- PAYLOAD +buffArray :: forall a . DType a => TypeRep a -> Handle -> Integer -> [IO a] +buffArray _ _ 0 = [] +buffArray t h i = do + let buffed = (buffElement h) : buffArray t h (i-1) + case eqTypeRep (typeOf buffed) (typeRep @[IO a]) of + Just HRefl -> buffed + _ -> error "Given TypeRep does not match data type." + +-- todo take malloc size dynamically +buffElement :: DType a => Handle -> IO a +buffElement h = do + ptr <- mallocBytes 8 + _ <- hGetBuf h (ptr) (sizeOf (undefined :: Int)) + val <- peek ptr + pure val + +loadPayload :: forall a . DType a => FilePath -> TypeRep a -> IO NdArray +loadPayload = undefined + +-- | Loads an NdArray from a .npy file +loadNpy :: forall a . DType a => FilePath -> TypeRep a -> IO NdArray +loadNpy path t = withBinaryFile path ReadMode $ \h -> do + -- Unpacks and parses the header to get the array type and size + descr <- hGetLine h + let + -- Places the dtype description, fortran order and shape in a map + metadata = (M.fromList . pairDict . listDict) descr + -- Extracts the dtype description e.g. traverse id buff + _ -> error "" + -} + l <- traverse id $ buffArray (typeRep @a) h sz + pure $ NdArray sh (V.fromList l) + + + --case eqTypeRep (typeOf buff) (typeRep @([IO a])) of + -- Just HRefl -> traverse id buff + -- _ -> error "" + {- + l <- traverse id (buffArray h sz) + case eqTypeRep (typeOf l) (typeRep @(IO (V.Vector a))) of + Just HRefl -> pure $ NdArray sh (V.fromList l) + _ -> error "" + -} + {-} + let v = V.fromList l + -- Converts the list & shape to an NdArray + case eqTypeRep (typeOf v) (typeRep @(V.Vector a)) of + Just HRefl -> pure $ NdArray sh v + _ -> error "urghfjd" +-} + +{- +thing :: DType a => Handle -> Integer -> String -> [IO a] +thing h sz dt = traverse id $ buffStuffs h sz + --" + --" + " buffStuffs h sz + --" + --" + --" + _ -> error "Unsupported dtype." +-} + +{- -- Reads an Int value from the binary accessed via the handle buffInt :: Handle -> IO Int buffInt h = do - ptr <- mallocBytes 4 - _ <- hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined ::Int)) + ptr <- mallocBytes 8 + _ <- hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined :: Int)) val <- peek ptr pure val @@ -81,31 +172,42 @@ buffInts :: Handle -> Integer -> [IO Int] buffInts _ 0 = [] buffInts h i = do (buffInt h) : buffInts h (i-1) --- NB: Currently only works with Ints specifically but this is easy to extend :) Working on it --- Loads an NdArray from a .npy file -loadNpy :: FilePath -> IO NdArray -loadNpy path = withBinaryFile path ReadMode $ \h -> do - -- Unpacks and parses the header to get the array type and size - descr <- hGetLine h - let - attrs = splitOn ":" $ (splitOn "{" descr) !! 1 - dtype = takeWhile (/='\'') $ drop 2 $ attrs !! 1 - shapeStrs = splitOn "," $ takeWhile (/=')') $ drop 2 $ attrs !! 3 - shape = map (\x -> read x :: Integer) $ take (length shapeStrs -1) shapeStrs - size = product shape - -- Reads the array itself into a list - let lio = buffInts h size - l <- traverse id lio - -- Converts the list & shape to an NdArray - pure $ NdArray shape (S.fromList l :: S.Vector Int) - +-- to do malloc with size of then you can try and pass it something generic +-- Reads an Int value from the binary accessed via the handle +buffFloat :: Handle -> IO Float +buffFloat h = do + ptr <- mallocBytes 4 + _ <- hGetBuf h (ptr :: Ptr Float) (sizeOf (undefined :: Float)) + val <- peek ptr + pure val +buffFloats :: DType a => Handle -> Integer -> [IO a] +buffFloats _ 0 = [] +buffFloats h i = do (buffFloat h) : buffFloats h (i-1) +-} -- Try it! It will probably break easily testsave :: IO () -testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (S.fromList [1,2,3 :: Int]) ) +testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Int]) ) + testload :: IO () testload = do - nd <- loadNpy "./src/testout/test123.npy" - putStrLn $ show $ nd \ No newline at end of file + nd <- loadNpy "./testout/test123.npy" (typeOf (1::Int)) + putStrLn $ show $ nd + + +{- +-- Reads the array itself into a list + l <- traverse id $ case dtype of + " buffInts h sz + --" + --" + " buffFloats h sz + --" + --" + --" + _ -> error "Unsupported dtype." + -- Converts the list & shape to an NdArray + pure $ NdArray sh (V.fromList l) +-} \ No newline at end of file From 43ceb84fef3c498f6d00679ca6438a1e2299c313 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 25 Jul 2023 17:28:17 +0100 Subject: [PATCH 36/90] Another serialisation update --- src/Serialisation.hs | 115 ++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 79 deletions(-) diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 48fe818..1e390a9 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -1,5 +1,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ImpredicativeTypes #-} + module Serialisation where @@ -84,7 +86,33 @@ pairDict [] = [] pairDict (_:[]) = [] pairDict (k:v:ls) = (k, v) : pairDict ls +--pyToTypeRep " String -> TypeRep a + +{- +pyToTypeRep :: forall a . String -> (DType a => TypeRep a) +pyToTypeRep dtype = case dtype of + " typeRep @Int :: TypeRep a + --" + " typeRep @Float + --" + --" + --" + _ -> error "Unsupported dtype." +-} + -- PAYLOAD +-- Read in an element from the handle +buffElement :: forall a . DType a => Handle -> IO a +buffElement h = do + let elemSize = sizeOf (undefined :: a) + ptr <- mallocBytes elemSize + _ <- hGetBuf h ptr elemSize + val <- peek ptr + pure val + +-- Read in the complete array as a list from the handle buffArray :: forall a . DType a => TypeRep a -> Handle -> Integer -> [IO a] buffArray _ _ 0 = [] buffArray t h i = do @@ -93,19 +121,13 @@ buffArray t h i = do Just HRefl -> buffed _ -> error "Given TypeRep does not match data type." --- todo take malloc size dynamically -buffElement :: DType a => Handle -> IO a -buffElement h = do - ptr <- mallocBytes 8 - _ <- hGetBuf h (ptr) (sizeOf (undefined :: Int)) - val <- peek ptr - pure val - -loadPayload :: forall a . DType a => FilePath -> TypeRep a -> IO NdArray -loadPayload = undefined +loadPayload :: forall a . DType a => Handle -> [Integer] -> TypeRep a -> IO NdArray +loadPayload h sh t = do + l <- traverse id $ buffArray (typeRep @a) h (product sh) + pure $ NdArray sh (V.fromList l) -- | Loads an NdArray from a .npy file -loadNpy :: forall a . DType a => FilePath -> TypeRep a -> IO NdArray +loadNpy :: DType a => FilePath -> TypeRep a -> IO NdArray loadNpy path t = withBinaryFile path ReadMode $ \h -> do -- Unpacks and parses the header to get the array type and size descr <- hGetLine h @@ -118,85 +140,20 @@ loadNpy path t = withBinaryFile path ReadMode $ \h -> do shapeStrs = filter (/= "") $ splitOn "," $ filter (`notElem`"()") (metadata M.! "shape:") sh = map (read @Integer) shapeStrs -- Calculates the total number of elements in the array - sz = product sh + --sz = product sh -- Reads the array itself into a list - {- kinda works - let buff = buffArray (typeRep @Int) h sz - case eqTypeRep (typeOf buff) (typeRep @[IO a]) of - Just HRefl -> traverse id buff - _ -> error "" - -} - l <- traverse id $ buffArray (typeRep @a) h sz - pure $ NdArray sh (V.fromList l) + loadPayload h sh t - - --case eqTypeRep (typeOf buff) (typeRep @([IO a])) of - -- Just HRefl -> traverse id buff - -- _ -> error "" - {- - l <- traverse id (buffArray h sz) - case eqTypeRep (typeOf l) (typeRep @(IO (V.Vector a))) of - Just HRefl -> pure $ NdArray sh (V.fromList l) - _ -> error "" - -} - {-} - let v = V.fromList l - -- Converts the list & shape to an NdArray - case eqTypeRep (typeOf v) (typeRep @(V.Vector a)) of - Just HRefl -> pure $ NdArray sh v - _ -> error "urghfjd" --} - -{- -thing :: DType a => Handle -> Integer -> String -> [IO a] -thing h sz dt = traverse id $ buffStuffs h sz - --" - --" - " buffStuffs h sz - --" - --" - --" - _ -> error "Unsupported dtype." --} - -{- --- Reads an Int value from the binary accessed via the handle -buffInt :: Handle -> IO Int -buffInt h = do - ptr <- mallocBytes 8 - _ <- hGetBuf h (ptr :: Ptr Int) (sizeOf (undefined :: Int)) - val <- peek ptr - pure val - -buffInts :: Handle -> Integer -> [IO Int] -buffInts _ 0 = [] -buffInts h i = do (buffInt h) : buffInts h (i-1) - --- to do malloc with size of then you can try and pass it something generic --- Reads an Int value from the binary accessed via the handle -buffFloat :: Handle -> IO Float -buffFloat h = do - ptr <- mallocBytes 4 - _ <- hGetBuf h (ptr :: Ptr Float) (sizeOf (undefined :: Float)) - val <- peek ptr - pure val - -buffFloats :: DType a => Handle -> Integer -> [IO a] -buffFloats _ 0 = [] -buffFloats h i = do (buffFloat h) : buffFloats h (i-1) --} -- Try it! It will probably break easily testsave :: IO () testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Int]) ) - testload :: IO () testload = do - nd <- loadNpy "./testout/test123.npy" (typeOf (1::Int)) + nd <- loadNpy "./testout/test123.npy" (typeRep @Int) putStrLn $ show $ nd - {- -- Reads the array itself into a list l <- traverse id $ case dtype of From 1424d1f9d9001019188ab007cdff0e3ce12637f5 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 26 Jul 2023 16:45:41 +0100 Subject: [PATCH 37/90] Transposition and more serialisation Co-authored-by: Basile Henry --- src/Numskull.hs | 40 ++++++++++++++++++++++++++++++++++++++++ src/Serialisation.hs | 29 ++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 28950d6..c555d3f 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -17,6 +17,7 @@ import Data.Vector.Storable (Vector) import Type.Reflection import qualified Data.Map as M import Data.Maybe (fromJust) +import Data.List (sort, elemIndex) -- $setup -- >>> import Numskull as N @@ -462,6 +463,45 @@ matMulElem map1 map2 ks (i:j:_) = foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.identity ks --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] +foldrArray :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b +foldrArray f z (NdArray _ v) = + case v =@= (undefined :: Vector a) of + Just HRefl -> V.foldr f z v + _ -> error "Starting value type does not match array type." + +dot :: DType a => NdArray -> NdArray -> a +dot nd1 nd2 = foldrArray (DType.add) (DType.identity) (nd1*nd2) + +-- reverse the order of axes +transpose :: NdArray -> NdArray +transpose = undefined + +-- Helper applies a permutation to a list +permuteList :: [Int] -> [a] -> [a] +permuteList perm l = if sort perm /= [0 .. length l -1] + then error "Invalid permutation given." + else map (l!!) perm + +-- Helper which finds the inverse of a permutation +invertPermutation :: [Int] -> [Int] +invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -1] + +-- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) +transposePerm perm (NdArray sh v) = + let + sh' = permuteList perm sh + perm' = invertPermutation perm + (_, toV) = mapIndicies sh + (fromU, _) = mapIndicies sh' + sz = V.length v + in NdArray sh' $ V.generate sz (\i -> + let + multU = fromU M.! i + flatV = toV M.! (permuteList perm' multU) + in v V.! flatV) + +determinant :: DType a => NdArray -> a +determinant = undefined -- * Common Errors shapeMismatch :: String -> String -> String diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 1e390a9..8001af5 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -1,6 +1,7 @@ {-# LANGUAGE GADTs #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -{-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE RankNTypes #-} module Serialisation where @@ -9,6 +10,7 @@ import Numskull import DType import NdArray +import Data.Int import System.IO import Data.List as List import Data.List.Split @@ -74,7 +76,6 @@ saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do V.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) - -- | PYTHON TO HASKELL | -- -- METADATA @@ -126,9 +127,23 @@ loadPayload h sh t = do l <- traverse id $ buffArray (typeRep @a) h (product sh) pure $ NdArray sh (V.fromList l) +-- todo check unicode UTF +reifyDType :: String -> (forall a . DType a => TypeRep a -> r) -> r +reifyDType dtype cont = + case dtype of + -- " cont (typeRep @Int64) + " cont (typeRep @Int) + -- " cont (typeRep @Int32) + " cont (typeRep @Float) + -- " cont (typeRep @Double) + -- " cont (typeRep @Char) + " cont (typeRep @Bool) + _ -> error "Unsupported dtype." + + -- | Loads an NdArray from a .npy file -loadNpy :: DType a => FilePath -> TypeRep a -> IO NdArray -loadNpy path t = withBinaryFile path ReadMode $ \h -> do +loadNpy :: FilePath -> IO NdArray +loadNpy path = withBinaryFile path ReadMode $ \h -> do -- Unpacks and parses the header to get the array type and size descr <- hGetLine h let @@ -142,16 +157,16 @@ loadNpy path t = withBinaryFile path ReadMode $ \h -> do -- Calculates the total number of elements in the array --sz = product sh -- Reads the array itself into a list - loadPayload h sh t + reifyDType dtype (loadPayload h sh) -- Try it! It will probably break easily testsave :: IO () -testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Int]) ) +testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Float]) ) testload :: IO () testload = do - nd <- loadNpy "./testout/test123.npy" (typeRep @Int) + nd <- loadNpy "./src/testout/test123.npy" putStrLn $ show $ nd {- From e33f8a9fae88d815a4e2c30efcbce792863649fb Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 26 Jul 2023 17:33:42 +0100 Subject: [PATCH 38/90] working on determinants --- src/Numskull.hs | 19 +++++++++++++++++-- src/Serialisation.hs | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index c555d3f..d706376 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -500,8 +500,23 @@ transposePerm perm (NdArray sh v) = flatV = toV M.! (permuteList perm' multU) in v V.! flatV) -determinant :: DType a => NdArray -> a -determinant = undefined +-- Numpy only defines this as sets over the 2D square matricies +-- If the matrix is non-square it is assumed to be padded out and will have det = 0 +-- https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html +determinant :: NdArray -> [Int] +determinant (NdArray s v) = case s of + [] -> [] + [x] -> [0] + [x,y] | x =/ y -> [0] + [x,y] -> [determinant2D (NdArray s v)] + _ -> undefined + +-- For a 2D matrix +determinant2D :: NdArray -> Int +determinant2D (NdArray s v) = case s of + [x,y] | x == y -> error "yes" + [x,y] | x =/ y -> 0 + _ -> error "no" -- * Common Errors shapeMismatch :: String -> String -> String diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 8001af5..445f3d6 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -161,6 +161,7 @@ loadNpy path = withBinaryFile path ReadMode $ \h -> do -- Try it! It will probably break easily +-- withTempFile testsave :: IO () testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Float]) ) From 83d67a378a8e1ca602f225b8dd851871312d52ed Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 27 Jul 2023 17:02:53 +0100 Subject: [PATCH 39/90] 2d determinant done compiles but untested --- src/Numskull.hs | 129 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 10 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index d706376..4979b30 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -37,6 +37,12 @@ ty = typeOf (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) +-- Helper asserting a type +(<-@) ::Typeable a => a -> TypeRep b -> b +(<-@) val t = case eqTypeRep t (ty val) of + Just HRefl -> val + _ -> error "Mismatching type." + -- Todo: show in a nicer shapely form :) instance Show NdArray where show (NdArray s v) = show s <> " " <> show v @@ -500,23 +506,126 @@ transposePerm perm (NdArray sh v) = flatV = toV M.! (permuteList perm' multU) in v V.! flatV) +-- only swaps the 'front' matrix +swapRows :: Integer -> Integer -> NdArray -> NdArray +swapRows r1 r2 (NdArray s v) + | r1 == r2 = (NdArray s v) + | length s < 2 = error "Too few rows to make swaps." + | r1 >= numRows || r2 >= numRows = error "Row index exceeds number of rows." + | otherwise = + let + lenRows = fromIntegral @Integer @Int $ s !! (colI+1) + rowInd1 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r1,0] + rowInds1 = V.iterateN lenRows succ rowInd1 + rowInd2 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r2,0] + rowInds2 = V.iterateN lenRows succ rowInd2 + row1 = V.slice rowInd1 lenRows v + row2 = V.slice rowInd2 lenRows v + in + NdArray s $ V.update_ v (rowInds2 V.++ rowInds1) (row1 V.++ row2) + where + colI = length s -2 + numRows = s !! colI + +frontColumn :: forall a . DType a => Int -> [Integer] -> Vector a -> Vector a +frontColumn col s v = V.ifilter + (\i _ -> i `mod` rowLen == col && i < rowLen*columns) $ + v <-@ (typeRep @(Vector a)) + where + rowLen = fromIntegral @Integer @Int $ s!!(length s -1) + columns = fromIntegral @Integer @Int $ s!!(length s -2) + +leadingDiagonal :: forall a . DType a => [Integer] -> Vector a -> Vector a +leadingDiagonal s v = + V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) v + where + rowLen = fromIntegral @Integer @Int $ s!!(length s -1) + columns = fromIntegral @Integer @Int $ s!!(length s -2) + +zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool +zeroRowVec r v = + let + ident = DType.identity :: a + (row, rest) = V.splitAt r v + in + (not $ V.null v) && + ((V.all (==ident) row) || + (zeroRowVec r rest)) + +-- note applies across whole array not just front +zeroRow :: NdArray -> Bool +zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v + +-- Todo check rows for singularity too +-- Note hangs if given a matrix with a zero-row +swapRowsWith0Pivot :: NdArray -> Maybe NdArray +swapRowsWith0Pivot (NdArray s v) = + let + diag = leadingDiagonal s v + ident = indentityElem' diag + in + case V.elemIndex ident diag of + -- x is the column-index of the 0 pivot + Just c -> case V.findIndex (/= ident) (frontColumn c s v) of + -- Swap 0-pivot and non-0 rows & try again + Just x -> swapRowsWith0Pivot $ + swapRows (fromIntegral x) (fromIntegral c) (NdArray s v) + -- The matrix is singular + Nothing -> Nothing + -- There is no 0-pivot + Nothing -> Just (NdArray s v) + -- Numpy only defines this as sets over the 2D square matricies -- If the matrix is non-square it is assumed to be padded out and will have det = 0 -- https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html -determinant :: NdArray -> [Int] +determinant :: forall a . DType a => NdArray -> [a] determinant (NdArray s v) = case s of [] -> [] - [x] -> [0] - [x,y] | x =/ y -> [0] - [x,y] -> [determinant2D (NdArray s v)] + [_] -> [DType.identity :: a] + [_,_] -> [determinant2D (NdArray s v)] _ -> undefined --- For a 2D matrix -determinant2D :: NdArray -> Int -determinant2D (NdArray s v) = case s of - [x,y] | x == y -> error "yes" - [x,y] | x =/ y -> 0 - _ -> error "no" +sequentialUpdate :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> Vector a +sequentialUpdate _ v [] = v +sequentialUpdate m v ((i,j,k) : trv) = + let + jk = m M.! [j,k] + ratio = DType.div (vecInd m v [j,i]) (vecInd m v [i,i]) + scaled = DType.multiply ratio (vecInd m v [i,k]) + newVjk = DType.subtract (vecInd m v [j,k]) scaled + in + sequentialUpdate m (v V.// [(jk, newVjk)]) trv + +-- For a 2D matrix using LU Decomposition as described here: +-- https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/Makalah-Matdis-2016-051.pdf +determinant2D :: forall a . DType a => NdArray -> a +determinant2D nd = + case shape nd of + [2,2] -> determinant2x2 nd + [c,r] | c == r && (not $ zeroRow nd) -> case swapRowsWith0Pivot nd of + Just (NdArray s v) -> + let + (_, fromMulti) = mapIndicies s + trv = [(i,j,k) | i <- [1..r], j <- [(i+1)..r], k <- [1..r]] + upperTriangle = sequentialUpdate fromMulti v trv + pivots = leadingDiagonal s upperTriangle + in + foldrArray (DType.multiply) (DType.identity :: a) (NdArray [fromIntegral $ V.length pivots] pivots) + + Nothing -> DType.identity :: a + [c,r] -> DType.identity :: a + _ -> error "Given matrix is not 2D." + +-- hidden helper +determinant2x2 :: forall a . DType a => NdArray -> a +determinant2x2 (NdArray _ v) = + let + mulI i1 i2 = DType.multiply (v V.! i1) (v V.! i2) + det = mulI 0 3 `DType.subtract` mulI 1 2 + in + det <-@ (typeRep @a) + + -- * Common Errors shapeMismatch :: String -> String -> String From 37c34ad38c398adc7ff21b20a772e0f134e9c26e Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 28 Jul 2023 11:28:15 +0100 Subject: [PATCH 40/90] determinants works --- src/DType.hs | 28 ++++++++------- src/Numskull.hs | 71 ++++++++++++++++++++++++------------- test/Test/upper_triangle.py | 19 ++++++++++ 3 files changed, 81 insertions(+), 37 deletions(-) create mode 100644 test/Test/upper_triangle.py diff --git a/src/DType.hs b/src/DType.hs index 5c65ca2..908321b 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -9,13 +9,14 @@ import GHC.Float (float2Double) -- Basis for all pointwise operations class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where - identity :: a + addId :: a + multId :: a -- Numeric add :: a -> a -> a subtract :: a -> a -> a multiply :: a -> a -> a - divide :: a -> a -> Double - div :: a -> a -> a + divide :: a -> a -> a + div :: a -> a -> Integer power :: a -> Double -> Double pow :: a -> a -> a log :: a -> a -> a @@ -38,13 +39,14 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where instance DType Int where - identity = 0 + addId = 0 + multId = 1 -- Numeric add x y = x + y subtract x y = x - y multiply x y = x * y - divide x y = fromIntegral x / fromIntegral y - div = P.div + divide x y = P.div x y + div x y = (fromIntegral $ P.div x y) :: Integer power x d = fromIntegral x ** d pow x y = x ^ y log x y = (P.floor $ logBase xd yd) :: Int @@ -73,13 +75,14 @@ roundIntFunc f x = (round $ f $ fromIntegral @Int @Float x) :: Int --instance DType Int64 where instance DType Float where - identity = 0.0 + addId = 0.0 + multId = 1.0 -- Numeric add x y = x + y subtract x y = x - y multiply x y = x * y - divide x y = float2Double (x / y) - div x y = fromIntegral (P.floor x `P.div` P.floor y) :: Float + divide x y = x/y + div x y = P.floor x `P.div` P.floor y power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y @@ -104,13 +107,14 @@ instance DType Float where --instance DType Double instance DType Bool where - identity = False + addId = False + multId = True -- Numeric add x y = x || y subtract x y = (x || y) && not (x && y) multiply x y = x && y - divide _x _y = undefined - div x y = not (x && y) + divide x y = not (x && y) + div x y = 0 power _x _d = undefined pow x y = toEnum (fromEnum x ^ fromEnum y) log _x _y = undefined diff --git a/src/Numskull.hs b/src/Numskull.hs index 4979b30..1e0965f 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -115,7 +115,7 @@ indentityElem = undefined -- Helper for the vectors in identityElem indentityElem' :: forall a . DType a => Vector a -> a -indentityElem' _ = DType.identity :: DType a => a +indentityElem' _ = DType.addId :: DType a => a -- | Creates an NdArray from a given shape and list. The number of elements must match. -- >>> printArray $ fromList [2,2] [1,2,3,4::Int] @@ -161,7 +161,7 @@ squareArr = undefined zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray zeros _ s = NdArray s zerovec where - ident = (DType.identity :: DType a => a) + ident = (DType.addId :: DType a => a) zerovec = (V.replicate (size s) ident) :: DType a => Vector a @@ -237,7 +237,7 @@ value for the array e.g. 0. To avoid this use !?. (#!) :: DType a => NdArray -> [Integer] -> a (NdArray s v) #! i = case (NdArray s v) !? i of Just val -> val - Nothing -> DType.identity :: DType a => a + Nothing -> DType.addId :: DType a => a {- | The safer version of #! which returns Nothing if an index exceeds the shape bounds. -} -- >>> m = fromListFlat [2,4,8 :: Int] @@ -329,9 +329,9 @@ pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then --elemDivide :: NdArray -> NdArray -> NdArray --elemDivide = pointwiseZip divide --- | Pointwise integer division -elemDiv :: NdArray -> NdArray -> NdArray -elemDiv = pointwiseZip DType.div +-- | Pointwise division +elemDivide :: NdArray -> NdArray -> NdArray +elemDivide = pointwiseZip DType.divide -- Todo: Needs to operate on doubles --elemPower :: NdArray -> NdArray -> NdArray @@ -379,7 +379,7 @@ constrainSize s v = -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) padSize :: DType a => Integer -> Vector a -> Vector a -padSize s v = v V.++ V.replicate ((fromIntegral s ::Int) - len) DType.identity +padSize s v = v V.++ V.replicate ((fromIntegral s ::Int) - len) DType.addId where len = V.length v -- Contrain or pad the vector to match the size @@ -466,7 +466,7 @@ matMulVec s v r u = matMulElem :: DType a => ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a matMulElem map1 map2 ks (i:j:_) = - foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.identity ks + foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] foldrArray :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b @@ -476,7 +476,7 @@ foldrArray f z (NdArray _ v) = _ -> error "Starting value type does not match array type." dot :: DType a => NdArray -> NdArray -> a -dot nd1 nd2 = foldrArray (DType.add) (DType.identity) (nd1*nd2) +dot nd1 nd2 = foldrArray (DType.add) (DType.addId) (nd1*nd2) -- reverse the order of axes transpose :: NdArray -> NdArray @@ -545,7 +545,7 @@ leadingDiagonal s v = zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool zeroRowVec r v = let - ident = DType.identity :: a + ident = DType.addId :: a (row, rest) = V.splitAt r v in (not $ V.null v) && @@ -578,42 +578,51 @@ swapRowsWith0Pivot (NdArray s v) = -- Numpy only defines this as sets over the 2D square matricies -- If the matrix is non-square it is assumed to be padded out and will have det = 0 -- https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html +{-} determinant :: forall a . DType a => NdArray -> [a] determinant (NdArray s v) = case s of [] -> [] - [_] -> [DType.identity :: a] + [_] -> [DType.addId :: a] [_,_] -> [determinant2D (NdArray s v)] _ -> undefined +-} + +upperTriangle :: NdArray -> NdArray +upperTriangle (NdArray s v) = + let + (_, fromMulti) = mapIndicies s + traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] + in + sequentialUpdate fromMulti v trv (indentityElem' v) -sequentialUpdate :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> Vector a -sequentialUpdate _ v [] = v -sequentialUpdate m v ((i,j,k) : trv) = +sequentialUpdate :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> a -> Vector a +sequentialUpdate _ v [] _ = v +sequentialUpdate m v ((i,j,k) : trv) r = let jk = m M.! [j,k] - ratio = DType.div (vecInd m v [j,i]) (vecInd m v [i,i]) + ratio = if k == 0 then DType.divide (vecInd m v [j,i]) (vecInd m v [i,i]) else r scaled = DType.multiply ratio (vecInd m v [i,k]) newVjk = DType.subtract (vecInd m v [j,k]) scaled in - sequentialUpdate m (v V.// [(jk, newVjk)]) trv + sequentialUpdate m (v V.// [(jk, newVjk)]) trv ratio -- For a 2D matrix using LU Decomposition as described here: -- https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/Makalah-Matdis-2016-051.pdf -determinant2D :: forall a . DType a => NdArray -> a +determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = case shape nd of - [2,2] -> determinant2x2 nd + [2,2] -> DType.addId [c,r] | c == r && (not $ zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> let (_, fromMulti) = mapIndicies s - trv = [(i,j,k) | i <- [1..r], j <- [(i+1)..r], k <- [1..r]] - upperTriangle = sequentialUpdate fromMulti v trv - pivots = leadingDiagonal s upperTriangle + trv = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] + upperTriangle = sequentialUpdate fromMulti v trv (indentityElem' v) + pivots = (leadingDiagonal s upperTriangle) <-@ typeRep @(Vector a) in - foldrArray (DType.multiply) (DType.identity :: a) (NdArray [fromIntegral $ V.length pivots] pivots) - - Nothing -> DType.identity :: a - [c,r] -> DType.identity :: a + V.foldr (DType.multiply) (DType.multId :: a) pivots + Nothing -> DType.addId + [_,_] -> DType.addId _ -> error "Given matrix is not 2D." -- hidden helper @@ -640,3 +649,15 @@ ndt2 :: NdArray ndt2 = fromList [2,3] [0,2,4,6,8,10::Int] nd3 = fromList [2,2] [1,2,3,4 :: Int] + +nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] +-- det = -71 +{- + 1 0 0 2 5 1 +9/2 1 0 x 0 s 5/2 + 2 t 1 0 0 71/41 + +t = -12/41 +s = -41/2 +-} + diff --git a/test/Test/upper_triangle.py b/test/Test/upper_triangle.py new file mode 100644 index 0000000..b2bd120 --- /dev/null +++ b/test/Test/upper_triangle.py @@ -0,0 +1,19 @@ +import numpy as np + +x = np.array([2,5,1, 9,2,7, 4,16,3], dtype=float).reshape(3,3) +print(x) + +#first index is y +print(x[1,2]) +print() +for i in range(3): + for j in range(i+1,3): + ratio = x[j,i] / x[i,i] + for k in range(3): + x[j,k] = x[j,k] - ratio * x[i,k] + print(i,j,k, ratio) + + +print() +print(x) +print(np.linalg.det(x)) From e72d97f2f8ead32c6181ac4d51f5b8fde323268d Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 28 Jul 2023 13:44:41 +0100 Subject: [PATCH 41/90] tidying --- src/Numskull.hs | 311 +++++++++++++++++++++++++++--------------------- 1 file changed, 174 insertions(+), 137 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 1e0965f..c5430f7 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -100,6 +100,9 @@ size sh = (fromIntegral $ product sh) :: Int shape :: NdArray -> [Integer] shape (NdArray s _) = s +getVector :: forall a . DType a => NdArray -> Vector a +getVector (NdArray _ v) = v <-@ typeRep @(Vector a) + -- | Gets the TypeRep for the NdArray elements ndType :: forall a . DType a => NdArray -> TypeRep a ndType (NdArray _ v) = case v =@= (undefined :: Vector a) of @@ -266,14 +269,22 @@ value for the array e.g. 0. To avoid this use !?. ----- One Argument +{- | Near identical to a standard foldr instance, expect NdArrays do not have an explicit type. +Folds in row-major order. +-} +foldrA :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b +foldrA f z (NdArray _ v) = + case v =@= (undefined :: Vector a) of + Just HRefl -> V.foldr f z v + _ -> error "Starting value type does not match array type." + +-- | Near identical to a standard map implementation in row-major order. mapA :: forall a . forall b . (DType a, DType b) => (a -> b) -> NdArray -> NdArray mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of Just HRefl -> NdArray s (V.map f v) _ -> error "Function input does not match array type." ---mapA :: (forall a . forall b . (DType a, DType b) => a -> b) -> NdArray -> NdArray ---mapA f (NdArray s v) = NdArray s (V.map f v) - +-- | Maps functions which return the same type. mapTransform :: (forall a . DType a => a -> a) -> NdArray -> NdArray mapTransform f (NdArray s v) = NdArray s (V.map f v) @@ -436,6 +447,81 @@ padShape r (NdArray s v) = -- * Matrix Operations +-- ROWS, COLUMNS & DIAGONALS + +{- | Switches the rows at the two given indicies over. +NB: designed for 2x2 matricies so will only make swaps in the 'front' matrix of a tensor. +-} +swapRows :: Integer -> Integer -> NdArray -> NdArray +swapRows r1 r2 (NdArray s v) + | r1 == r2 = (NdArray s v) + | length s < 2 = error "Too few rows to make swaps." + | r1 >= numRows || r2 >= numRows = error "Row index exceeds number of rows." + | otherwise = + let + lenRows = fromIntegral @Integer @Int $ s !! (colI+1) + rowInd1 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r1,0] + rowInds1 = V.iterateN lenRows succ rowInd1 + rowInd2 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r2,0] + rowInds2 = V.iterateN lenRows succ rowInd2 + row1 = V.slice rowInd1 lenRows v + row2 = V.slice rowInd2 lenRows v + in + NdArray s $ V.update_ v (rowInds2 V.++ rowInds1) (row1 V.++ row2) + where + colI = length s -2 + numRows = s !! colI + +{- | Gets the flat array of the leading diagonal of the 'front' matrix of the tensor. -} +diagonal :: NdArray -> NdArray +diagonal (NdArray s v) = NdArray [V.length v'] v' + v' = diagonalVec s v + +-- Helper to take the leading diagonal in the vector form. +diagonalVec :: forall a . DType a => [Integer] -> Vector a -> Vector a +diagonalVec s v = + V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) v + where + rowLen = fromIntegral @Integer @Int $ s!!(length s -1) + columns = fromIntegral @Integer @Int $ s!!(length s -2) + +-- TRANSPOSITION + +-- | Reverses the order of axes and switches the elements accordingly. +transpose :: NdArray -> NdArray +transpose (NdArray sh v) = transposePerm [length sh -1..0] (NdArray sh v) + +-- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) +transposePerm :: [Int] -> NdArray -> NdArray +transposePerm perm (NdArray sh v) = + let + sh' = permuteList perm sh + perm' = invertPermutation perm + (_, toV) = mapIndicies sh + (fromU, _) = mapIndicies sh' + sz = V.length v + in NdArray sh' $ V.generate sz (\i -> + let + multU = fromU M.! i + flatV = toV M.! (permuteList perm' multU) + in v V.! flatV) + +-- Applies a permutation to a list +permuteList :: [Int] -> [a] -> [a] +permuteList perm l = if sort perm /= [0 .. length l -1] + then error "Invalid permutation given." + else map (l!!) perm + +-- Finds the inverse of a permutation +invertPermutation :: [Int] -> [Int] +invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -1] + +-- MULTIPLICATION + +-- | Dot product over matricies of the same shape. +dot :: DType a => NdArray -> NdArray -> a +dot nd1 nd2 = foldrA (DType.add) (DType.addId) (nd1*nd2) + -- For now, just nxm and mxp = nxp matMul :: NdArray -> NdArray -> NdArray matMul (NdArray s v) (NdArray r u) = @@ -469,172 +555,123 @@ matMulElem map1 map2 ks (i:j:_) = foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] -foldrArray :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b -foldrArray f z (NdArray _ v) = - case v =@= (undefined :: Vector a) of - Just HRefl -> V.foldr f z v - _ -> error "Starting value type does not match array type." - -dot :: DType a => NdArray -> NdArray -> a -dot nd1 nd2 = foldrArray (DType.add) (DType.addId) (nd1*nd2) - --- reverse the order of axes -transpose :: NdArray -> NdArray -transpose = undefined - --- Helper applies a permutation to a list -permuteList :: [Int] -> [a] -> [a] -permuteList perm l = if sort perm /= [0 .. length l -1] - then error "Invalid permutation given." - else map (l!!) perm - --- Helper which finds the inverse of a permutation -invertPermutation :: [Int] -> [Int] -invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -1] - --- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) -transposePerm perm (NdArray sh v) = - let - sh' = permuteList perm sh - perm' = invertPermutation perm - (_, toV) = mapIndicies sh - (fromU, _) = mapIndicies sh' - sz = V.length v - in NdArray sh' $ V.generate sz (\i -> - let - multU = fromU M.! i - flatV = toV M.! (permuteList perm' multU) - in v V.! flatV) - --- only swaps the 'front' matrix -swapRows :: Integer -> Integer -> NdArray -> NdArray -swapRows r1 r2 (NdArray s v) - | r1 == r2 = (NdArray s v) - | length s < 2 = error "Too few rows to make swaps." - | r1 >= numRows || r2 >= numRows = error "Row index exceeds number of rows." - | otherwise = - let - lenRows = fromIntegral @Integer @Int $ s !! (colI+1) - rowInd1 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r1,0] - rowInds1 = V.iterateN lenRows succ rowInd1 - rowInd2 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r2,0] - rowInds2 = V.iterateN lenRows succ rowInd2 - row1 = V.slice rowInd1 lenRows v - row2 = V.slice rowInd2 lenRows v - in - NdArray s $ V.update_ v (rowInds2 V.++ rowInds1) (row1 V.++ row2) - where - colI = length s -2 - numRows = s !! colI - -frontColumn :: forall a . DType a => Int -> [Integer] -> Vector a -> Vector a -frontColumn col s v = V.ifilter - (\i _ -> i `mod` rowLen == col && i < rowLen*columns) $ - v <-@ (typeRep @(Vector a)) - where - rowLen = fromIntegral @Integer @Int $ s!!(length s -1) - columns = fromIntegral @Integer @Int $ s!!(length s -2) - -leadingDiagonal :: forall a . DType a => [Integer] -> Vector a -> Vector a -leadingDiagonal s v = - V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) v - where - rowLen = fromIntegral @Integer @Int $ s!!(length s -1) - columns = fromIntegral @Integer @Int $ s!!(length s -2) - -zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool -zeroRowVec r v = - let - ident = DType.addId :: a - (row, rest) = V.splitAt r v - in - (not $ V.null v) && - ((V.all (==ident) row) || - (zeroRowVec r rest)) - --- note applies across whole array not just front -zeroRow :: NdArray -> Bool -zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v - --- Todo check rows for singularity too --- Note hangs if given a matrix with a zero-row -swapRowsWith0Pivot :: NdArray -> Maybe NdArray -swapRowsWith0Pivot (NdArray s v) = - let - diag = leadingDiagonal s v - ident = indentityElem' diag - in - case V.elemIndex ident diag of - -- x is the column-index of the 0 pivot - Just c -> case V.findIndex (/= ident) (frontColumn c s v) of - -- Swap 0-pivot and non-0 rows & try again - Just x -> swapRowsWith0Pivot $ - swapRows (fromIntegral x) (fromIntegral c) (NdArray s v) - -- The matrix is singular - Nothing -> Nothing - -- There is no 0-pivot - Nothing -> Just (NdArray s v) - --- Numpy only defines this as sets over the 2D square matricies --- If the matrix is non-square it is assumed to be padded out and will have det = 0 --- https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html -{-} -determinant :: forall a . DType a => NdArray -> [a] -determinant (NdArray s v) = case s of - [] -> [] - [_] -> [DType.addId :: a] - [_,_] -> [determinant2D (NdArray s v)] - _ -> undefined --} +-- DETERMINANTS & INVERSES +-- | Converts a nxn matrix to upper triangle form. O(n^3). upperTriangle :: NdArray -> NdArray -upperTriangle (NdArray s v) = +upperTriangle (NdArray (c:rs) v) = let - (_, fromMulti) = mapIndicies s + (_, fromMulti) = mapIndicies (c:rs) traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] in - sequentialUpdate fromMulti v trv (indentityElem' v) + NdArray (c:rs) $ triangulateVec fromMulti v traversals (indentityElem' v) -sequentialUpdate :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> a -> Vector a -sequentialUpdate _ v [] _ = v -sequentialUpdate m v ((i,j,k) : trv) r = +-- Upper triangle form on the hidden vector. +triangulateVec :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> a -> Vector a +triangulateVec _ v [] _ = v +triangulateVec m v ((i,j,k) : trv) r = let jk = m M.! [j,k] ratio = if k == 0 then DType.divide (vecInd m v [j,i]) (vecInd m v [i,i]) else r scaled = DType.multiply ratio (vecInd m v [i,k]) newVjk = DType.subtract (vecInd m v [j,k]) scaled in - sequentialUpdate m (v V.// [(jk, newVjk)]) trv ratio + triangulateVec m (v V.// [(jk, newVjk)]) trv ratio --- For a 2D matrix using LU Decomposition as described here: --- https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/Makalah-Matdis-2016-051.pdf +{- | Finds the determinant(s) of a tensor. Over matricies of more than two dimensions +each 2D matrix's determinant is individually calculated and concatenated together (as in numpy: +https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html ). +If the matrix is non-square it is assumed to be padded out and will have determinant of 0 +-} +determinant :: forall a . DType a => NdArray -> [a] +determinant (NdArray s v) = case s of + [] -> [] + [_] -> [DType.addId :: a] + [_,_] -> [determinant2D (NdArray s v)] + _ | V.null v -> [] + _ -> + let + (c,r) = (s!!(length s -2), last s) + (twoDim, rest) = V.splitAt (fromIntegral$c*r) v + in (determinant2D (NdArray [c,r] twoDim) : determinant (NdArray s rest)) + +{- | Calculates the determinant of a 2D matrix using LU decomposition as described in the +below paper. O(n^3). +https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/Makalah-Matdis-2016-051.pdf +-} determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = case shape nd of - [2,2] -> DType.addId + -- 2x2 matricies are calculated quickly with the standard ad-bc + [2,2] -> determinant2x2 nd + -- nxn matricies are row-swapped to find an arrangement with no zeros/identity elements + -- in the leading diagonal (pivots) then put into upper triangle form [c,r] | c == r && (not $ zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> let - (_, fromMulti) = mapIndicies s - trv = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] - upperTriangle = sequentialUpdate fromMulti v trv (indentityElem' v) - pivots = (leadingDiagonal s upperTriangle) <-@ typeRep @(Vector a) + upperTri = upperTriangle (NdArray s v) + upperTriV = getVector upperTri :: Vector a + pivots = diagonalVec s upperTriV in + -- determinant is the product of the pivots in upper triangle form V.foldr (DType.multiply) (DType.multId :: a) pivots + -- If the matrix is non-square or has a zero-row/column, it is singular. Nothing -> DType.addId [_,_] -> DType.addId _ -> error "Given matrix is not 2D." --- hidden helper +-- 2x2 quick determinant calculation of ad-bc determinant2x2 :: forall a . DType a => NdArray -> a determinant2x2 (NdArray _ v) = let mulI i1 i2 = DType.multiply (v V.! i1) (v V.! i2) det = mulI 0 3 `DType.subtract` mulI 1 2 - in - det <-@ (typeRep @a) + in det <-@ (typeRep @a) + +-- | Checks the whole array for the prescence of a zero-row. +zeroRow :: NdArray -> Bool +zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v +-- Checks the array in vector form for a zero-row. +zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool +zeroRowVec r v = + let + ident = DType.addId :: a + (row, rest) = V.splitAt r v + in + (not $ V.null v) && + ((V.all (==ident) row) || + (zeroRowVec r rest)) +{- Repeatedly swaps rows until the matrix is found to be singular or +there are no pivots which are zero/identity elem. If singular, returns Nothing. +Note: hangs if given a matrix with a zero-row. +-} +swapRowsWith0Pivot :: NdArray -> Maybe NdArray +swapRowsWith0Pivot (NdArray s v) = + let + diag = diagonalVec s v + ident = indentityElem' diag + in + case V.elemIndex ident diag of + -- x is the column-index of the 0 pivot + Just c -> case V.findIndex (/= ident) (frontColumn c s v) of + -- Swap 0-pivot and non-0 rows & try again + Just x -> swapRowsWith0Pivot $ + swapRows (fromIntegral x) (fromIntegral c) (NdArray s v) + -- The matrix is singular + Nothing -> Nothing + -- There is no 0-pivot + Nothing -> Just (NdArray s v) + +{- Extracts the indexed column from the front matrix of a tensor given its shape and vector. -} +frontColumn :: forall a . DType a => Int -> [Integer] -> Vector a -> Vector a +frontColumn col s v = V.ifilter + (\i _ -> i `mod` rowLen == col && i < rowLen*columns) $ + v <-@ (typeRep @(Vector a)) + where + rowLen = fromIntegral @Integer @Int $ s!!(length s -1) + columns = fromIntegral @Integer @Int $ s!!(length s -2) -- * Common Errors shapeMismatch :: String -> String -> String From dcc7e0d48f789f28389a0d9c40edad1e449d9cde Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 28 Jul 2023 16:18:50 +0100 Subject: [PATCH 42/90] slicing --- src/Numskull.hs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index c5430f7..f42a37a 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -262,7 +262,40 @@ value for the array e.g. 0. To avoid this use !?. Nothing -> Nothing else Nothing --- Todo: slicing +slice :: [(Integer, Integer)] -> NdArray -> NdArray +slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) + where (m,_) = mapIndicies sh + +(!/) :: NdArray -> [(Integer, Integer)] -> NdArray +(!/) nd ss = slice ss nd + +-- helpser +sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray +sliceWithMap _ _ [] nd = nd +sliceWithMap _ d _ (NdArray sh v) | d >= length sh = (NdArray sh v) +sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ + sliceDim s d m (NdArray sh v) + +-- inclusive +sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray +sliceDim (x,y) d m (NdArray s v) = + if d >= length s then error "Given dimension does not exist in array." + else NdArray + (if y < x then [] else shrinkNth d (y-x+1) s) + (V.ifilter + (\i _ -> + let dimInd = (m M.! i) !! d + in x <= dimInd && dimInd <= y) + v + ) + +-- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell +shrinkNth :: Ord a => Int -> a -> [a] -> [a] +shrinkNth _ _ [] = [] +shrinkNth n newVal (x:xs) + | n == 0 = if newVal < x then newVal:xs else x:xs + | otherwise = x:shrinkNth (n-1) newVal xs + -- * Pointwise Functions -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances @@ -474,8 +507,8 @@ swapRows r1 r2 (NdArray s v) {- | Gets the flat array of the leading diagonal of the 'front' matrix of the tensor. -} diagonal :: NdArray -> NdArray -diagonal (NdArray s v) = NdArray [V.length v'] v' - v' = diagonalVec s v +diagonal (NdArray s v) = NdArray [fromIntegral $ V.length v'] v' + where v' = diagonalVec s v -- Helper to take the leading diagonal in the vector form. diagonalVec :: forall a . DType a => [Integer] -> Vector a -> Vector a From cf751b477d11f0aa57c0d301db0ed006ef747759 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 28 Jul 2023 17:01:21 +0100 Subject: [PATCH 43/90] dtype tidying --- src/DType.hs | 126 +++++++++++++++++++++++++++++++++++++++++-- src/NdArray.hs | 6 ++- src/Numskull.hs | 5 +- src/Serialisation.hs | 3 +- 4 files changed, 129 insertions(+), 11 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 908321b..77d63ec 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -6,6 +6,8 @@ import Prelude as P import Data.Vector.Storable import Type.Reflection import GHC.Float (float2Double) +import Data.Int +import Data.Char -- Basis for all pointwise operations class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where @@ -37,7 +39,6 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where dtypeToRational :: a -> Rational rationalToDtype :: Rational -> a - instance DType Int where addId = 0 multId = 1 @@ -72,7 +73,68 @@ instance DType Int where roundIntFunc :: (Float -> Float) -> Int -> Int roundIntFunc f x = (round $ f $ fromIntegral @Int @Float x) :: Int ---instance DType Int64 where +-- Entirely copied really +instance DType Int32 where + addId = 0 + multId = 1 + -- Numeric + add x y = x + y + subtract x y = x - y + multiply x y = x * y + divide x y = P.div x y + div x y = (fromIntegral $ P.div x y) :: Integer + power x d = fromIntegral x ** d + pow x y = x ^ y + log x y = (P.floor $ logBase xd yd) :: Int32 + where xd = fromIntegral @Int32 @Double x + yd = fromIntegral @Int32 @Double y + mod x y = fromIntegral (x `P.mod` y) :: Integer + abs = P.abs + signum = P.signum + ceil x = x + floor x = x + -- Trig + sin x = (round $ P.sin $ fromIntegral x) :: Int32 + cos x = (round $ P.sin $ fromIntegral x) :: Int32 + tan x = (round $ P.sin $ fromIntegral x) :: Int32 + -- Logical + invert x = -x + shiftleft x = x * 2 + shiftright x = x `P.div` 2 + -- (Conversions) + dtypeToRational = toRational + rationalToDtype = P.floor . fromRational @Double + +instance DType Int64 where + addId = 0 + multId = 1 + -- Numeric + add x y = x + y + subtract x y = x - y + multiply x y = x * y + divide x y = P.div x y + div x y = (fromIntegral $ P.div x y) :: Integer + power x d = fromIntegral x ** d + pow x y = x ^ y + log x y = (P.floor $ logBase xd yd) :: Int64 + where xd = fromIntegral @Int64 @Double x + yd = fromIntegral @Int64 @Double y + mod x y = fromIntegral (x `P.mod` y) :: Integer + abs = P.abs + signum = P.signum + ceil x = x + floor x = x + -- Trig + sin x = (round $ P.sin $ fromIntegral x) :: Int64 + cos x = (round $ P.sin $ fromIntegral x) :: Int64 + tan x = (round $ P.sin $ fromIntegral x) :: Int64 + -- Logical + invert x = -x + shiftleft x = x * 2 + shiftright x = x `P.div` 2 + -- (Conversions) + dtypeToRational = toRational + rationalToDtype = P.floor . fromRational @Double instance DType Float where addId = 0.0 @@ -104,7 +166,35 @@ instance DType Float where dtypeToRational = toRational rationalToDtype = fromRational @Float ---instance DType Double +instance DType Double where + addId = 0.0 + multId = 1.0 + -- Numeric + add x y = x + y + subtract x y = x - y + multiply x y = x * y + divide x y = x/y + div x y = P.floor x `P.div` P.floor y + power x d = x ** d + pow x y = x ** y + log x y = logBase x y + mod x y = fromIntegral (xi `P.mod` yi):: Integer + where xi = P.floor x; yi = P.floor y + abs = P.abs + signum = P.signum + ceil = fromIntegral @Integer @Double . P.ceiling + floor = fromIntegral @Integer @Double . P.floor + -- Trig + sin = P.sin + cos = P.cos + tan = P.tan + -- Logical + invert x = -x + shiftleft x = x * 2 + shiftright x = x / 2 + -- Conversion + dtypeToRational = toRational + rationalToDtype = fromRational @Double instance DType Bool where addId = False @@ -140,7 +230,35 @@ instance DType Bool where rationalToDtype 0 = False rationalToDtype _ = True ---instance DType Char where? +instance DType Char where + addId = '\NUL' + multId = 'a' + -- Numeric + add x y = chr $ ord x + ord y + subtract x y = chr $ min 0 $ ord x + ord y + multiply x y = undefined + divide x y = undefined + div x y = undefined + power x d = undefined + pow x y = undefined + log x y = undefined + mod x y = undefined + abs = undefined + signum c = if isAlpha c then if isUpper c then 'A' else 'a' + else if isDigit c then '0' else c + ceil = toUpper + floor = toLower + -- Trig + sin = undefined + cos = undefined + tan = undefined + -- Logical + invert c = if isUpper c then toLower c else toUpper c + shiftleft x = chr $ ord x + 1 + shiftright x = chr $ ord x - 1 + -- Conversion + dtypeToRational = toRational . ord + rationalToDtype = chr . P.floor. fromRational @Double --instance ByteString where? diff --git a/src/NdArray.hs b/src/NdArray.hs index b6d8360..ee87635 100644 --- a/src/NdArray.hs +++ b/src/NdArray.hs @@ -10,4 +10,8 @@ import Data.Vector.Storable -- | The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are -- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). data NdArray where - NdArray :: DType a => [Integer] -> Vector a -> NdArray \ No newline at end of file + NdArray :: DType a => [Integer] -> Vector a -> NdArray + +-- Todo: show in a nicer shapely form :) +instance Show NdArray where + show (NdArray s v) = show s <> " " <> show v \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index f42a37a..1c50f8a 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -11,6 +11,7 @@ import NdArray import qualified DType import DType (DType) import MatrixForm +import Serialisation import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) @@ -43,10 +44,6 @@ ty = typeOf Just HRefl -> val _ -> error "Mismatching type." --- Todo: show in a nicer shapely form :) -instance Show NdArray where - show (NdArray s v) = show s <> " " <> show v - instance Eq NdArray where -- | Arrays are equal if their elements and shape exactly match. (NdArray s v) == (NdArray r u) = (r == s) && diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 445f3d6..c47aa3b 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -6,7 +6,6 @@ module Serialisation where -import Numskull import DType import NdArray @@ -25,7 +24,7 @@ import Data.Word (Word16) -- | Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String -getNumpyDType (NdArray _ v) = case show $ ty v of +getNumpyDType (NdArray _ v) = case show $ typeOf v of "Vector Int" -> " " " Date: Mon, 31 Jul 2023 10:30:22 +0100 Subject: [PATCH 44/90] tidying --- DenseTest.hs | 12 ---- TypeableTest.hs | 113 -------------------------------------- app/Main.hs | 11 ---- rowan-ndarray.cabal | 130 -------------------------------------------- src/MyLib.hs | 4 -- tests/VectorTest.hs | 84 ---------------------------- 6 files changed, 354 deletions(-) delete mode 100644 DenseTest.hs delete mode 100644 TypeableTest.hs delete mode 100644 app/Main.hs delete mode 100644 rowan-ndarray.cabal delete mode 100644 src/MyLib.hs delete mode 100644 tests/VectorTest.hs diff --git a/DenseTest.hs b/DenseTest.hs deleted file mode 100644 index 9a83149..0000000 --- a/DenseTest.hs +++ /dev/null @@ -1,12 +0,0 @@ -module DenseTest where - -import Dense - -class Typeable a => Dtype a where - add :: a -> a -> a - mult :: a -> a -> a - eq :: a -> a -> Bool - ... - -data NdArray where - NdArray :: Dtype a => Array DynIx a -> NdArray \ No newline at end of file diff --git a/TypeableTest.hs b/TypeableTest.hs deleted file mode 100644 index 0a8e1d2..0000000 --- a/TypeableTest.hs +++ /dev/null @@ -1,113 +0,0 @@ ---language extension list --- https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/table.html - -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE AllowAmbiguousTypes #-} -{-# LANGUAGE TypeApplications #-} - - - -module TypeableTest where - ---import Data.Dense -import Data.Dynamic -import Data.Typeable -import Data.Vector -import Data.Array.Repa as R ---import Onnx.Representation.GraphDef - ---instance Typeable (Array U s ) - -{- -data NDArray typ where - NDArray :: (Typeable typ, Show typ, Shape sh) => - Array U sh typ -> NDArray typ -newtype NDArray a = NDArray (R.Array U s t) --} ---data NDArray2 = NDArray2 (Z :. Int :. ...) Dynamic - --- to fix w/type variable? https://downloads.haskell.org/~ghc/6.4/docs/html/users_guide/type-extensions.html ---data NDArray = NDAConstr { --- fromND :: (Shape sh) => Array U sh Dynamic ---} - ---deriving instance Show (NDArray a) ---instance Show (NDArray a) where --- show (NDArray x) = show x - ---t = NDArray (fromListUnboxed (Z :. (3::Int) :. (3::Int)) [1..9::Int]) - - - - - --- Dynamic type, a dype, have an instance of each supported type --- which defines how all the standard ops work --- Can you have optional functions defined here? e.g. bitshift -class Typeable a => Dype a where - add :: a -> a -> a - mult :: a -> a -> a - eq :: a -> a -> Bool - -- ... - --- You can provide a shape for the arrays but you can also not --- Need a function which works this out to convert a Dshape to a Sshape --- given the array -data Dshape = Dshape | Sshape [Int] deriving Show - --- Dshape is a hacky instance of Repa's shape so the NdArray definition --- won't complain. -instance Eq Dshape where - Dshape == Dshape = False - Sshape x == Sshape y = x == y - Dshape == Sshape _ = False - Sshape _ == Dshape = False -instance R.Shape Dshape where - rank _ = undefined - ---data NdArray where --- NdArray :: Dype a => Array U Dshape a -> NdArray - ---unwrapND :: Dype a => NdArray -> Array U Dshape a ---unwrapND (NdArray x) = x --- where intarr = --- if typeRep a == "Int" then --- arr :: Just (Array U Dshape Int) --- else Nothing - ---instance Show NdArray where --- show x | = show $ cast (Array U Dshape Int) - -data TypedNdArray type shape = TypedNdArray String [Int] - -data NdArray where - NdArray :: Dype a => Array U (Z :. Int) a -> NdArray - -instance Dype Int where - add x y = x + y - mult x y = x * y - eq x y = x == y - -unwrapND :: NdArray -> Maybe (Array U (Z :. Int) Int) -unwrapND (NdArray x) = Just x - -arr = fromListUnboxed (Z :. (2::Int)) [1,2::Int] -nd = NdArray arr -arr2 = unwrapND nd - - ---deriving instance Show (NdArray) - --- instance of Dypes for Float Int etc - -{- -addArrays :: NdArray -> NdArray -> NdArray -addArrays (NdArray x) (NdArray y) = case typeOf x `eqTypeRep` typeOf b of - Just HRefl -> NdArray (zipWith add x y) - Nothing -> throw ValueError -- or should this automatically convert? --} \ No newline at end of file diff --git a/app/Main.hs b/app/Main.hs deleted file mode 100644 index 22d7dd8..0000000 --- a/app/Main.hs +++ /dev/null @@ -1,11 +0,0 @@ -module Main where - -import qualified MyLib (someFunc) -import Dense - -x = V2 1 2 ^+^ V2 3 4 - -main :: IO () -main = do - putStrLn "Hello, Haskell!" - MyLib.someFunc diff --git a/rowan-ndarray.cabal b/rowan-ndarray.cabal deleted file mode 100644 index 8f7197c..0000000 --- a/rowan-ndarray.cabal +++ /dev/null @@ -1,130 +0,0 @@ -cabal-version: 2.4 --- The cabal-version field refers to the version of the .cabal specification, --- and can be different from the cabal-install (the tool) version and the --- Cabal (the library) version you are using. As such, the Cabal (the library) --- version used must be equal or greater than the version stated in this field. --- Starting from the specification version 2.2, the cabal-version field must be --- the first thing in the cabal file. - --- Initial package description 'rowan-ndarray' generated by --- 'cabal init'. For further documentation, see: --- http://haskell.org/cabal/users-guide/ --- --- The name of the package. -name: rowan-ndarray - --- The package version. --- See the Haskell package versioning policy (PVP) for standards --- guiding when and how versions should be incremented. --- https://pvp.haskell.org --- PVP summary: +-+------- breaking API changes --- | | +----- non-breaking API additions --- | | | +--- code changes with no API change -version: 0.1.0.0 - --- A short (one-line) description of the package. --- synopsis: - --- A longer description of the package. --- description: - --- The license under which the package is released. -license: MIT - --- The file containing the license text. -license-file: LICENSE - --- The package author(s). -author: Rowan Mather - --- An email address to which users can send suggestions, bug reports, and patches. -maintainer: rowan@myrtle.ai - --- A copyright notice. --- copyright: -category: Math -build-type: Simple - --- Extra doc files to be distributed with the package, such as a CHANGELOG or a README. -extra-doc-files: CHANGELOG.md - --- Extra source files to be distributed with the package, such as examples, or a tutorial module. --- extra-source-files: - -common warnings - ghc-options: -Wall - -library - -- Import common warning flags. - import: warnings - - -- Modules exported by the library. - exposed-modules: MyLib - - -- Modules included in this library but not exported. - -- other-modules: - - -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - - -- Other library packages from which modules are imported. - build-depends: base ^>=4.9.1.0 - - -- Directories containing source files. - hs-source-dirs: src - - -- Base language which the package is written in. - default-language: Haskell2010 - -executable rowan-ndarray - -- Import common warning flags. - import: warnings - - -- .hs or .lhs file containing the Main module. - main-is: Main.hs - - -- Modules included in this executable, other than Main. - -- other-modules: - - -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - - -- Other library packages from which modules are imported. - build-depends: - base ^>=4.9.1.0, - rowan-ndarray, - dense - - -- Directories containing source files. - hs-source-dirs: app - - -- Base language which the package is written in. - default-language: Haskell2010 - -test-suite rowan-ndarray-test - -- Import common warning flags. - import: warnings - - -- Base language which the package is written in. - default-language: Haskell2010 - - -- Modules included in this executable, other than Main. - -- other-modules: - - -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - - -- The interface type and version of the test suite. - type: exitcode-stdio-1.0 - - -- Directories containing source files. - hs-source-dirs: test - - -- The entrypoint to the test suite. - main-is: Main.hs - - -- Test dependencies. - build-depends: - base ^>=4.9.1.0, - rowan-ndarray, - dense diff --git a/src/MyLib.hs b/src/MyLib.hs deleted file mode 100644 index e657c44..0000000 --- a/src/MyLib.hs +++ /dev/null @@ -1,4 +0,0 @@ -module MyLib (someFunc) where - -someFunc :: IO () -someFunc = putStrLn "someFunc" diff --git a/tests/VectorTest.hs b/tests/VectorTest.hs deleted file mode 100644 index ea06fa1..0000000 --- a/tests/VectorTest.hs +++ /dev/null @@ -1,84 +0,0 @@ --- trust me bro ;) --- :set -fdefer-type-errors - -{-# LANGUAGE GADTs #-} - -module VectorTest where - -import Prelude as P -import Data.Vector as V -import Data.Dynamic -import Type.Reflection -import Data.Maybe (isJust, fromJust) - -ty x = typeOf x - --- DType -- -class (Show a, Typeable a) => DType a where - add :: a -> a -> a - subtract :: a -> a -> a - multiply :: a -> a -> a - eq :: a -> a -> Bool - dtypeToInt :: a -> Int - -instance DType Int where - add x y = x + y - multiply x y = x * y - eq x y = x == y - --- NdArray -- -data NdArray where - NdArray :: (Typeable a, DType a) => Vector a -> NdArray - -instance Show NdArray where - show (NdArray x) = show x - -instance Num NdArray where - (NdArray x) + (NdArray y) = case (eqTypeRep xtype ytype, matchDType (NdArray x) (NdArray y)) of - (Just HRefl, _) -> NdArray (V.zipWith add x y) -- Types match - (_, Just casted) -> (NdArray x) + casted -- Second type can be converted to first - otherwise -> error ("Cannot convert second matrix of type '" P.++ show ytype P.++ "' to type '" P.++ show xtype P.++ "'.") - where - xtype = ty x - ytype = ty y - - --(NdArray x) - (NDArray y) = - - --(NdArray x) * (NDArray y) - - --- Helper -eqDType x y = case eqTypeRep (ty x) (ty y) of - Just HRefl -> True - otherwise -> False - -matchDType :: NdArray -> NdArray -> Maybe NdArray -matchDType (NdArray x) (NdArray y) = case eqTypeRep (ty x) (ty (fromList [1::Int])) of - Just HRefl -> Just $ NdArray (V.map dtypeToInt y) - otherwise -> Nothing - - - - --- Spaghetti -{- -instance Num NdArray where - (NdArray x) + (NdArray y) = - case typeOf x `eqTypeRep` typeOf y of - Just HRefl -> NdArray (V.zipWith add x y) -- Types match - Nothing -> case casted of Just newNd -> (V.zipWith add x newNd) - where casted = matchDType (NdArray x) (NdArray y) --} - - - ----- Testing - -unwrapND :: NdArray -> (String, Vector Dynamic) -unwrapND (NdArray x) = case typeOf x of - vecTypeInt -> ("Int", V.map toDyn x) - vecTypeBool -> ("Bool", V.map toDyn x) - -nd1 = NdArray (fromList [1,2,3::Int]) -nd2 = NdArray (fromList [10,11,12::Int]) - From 3aad0e720fab70155d798def12c74c9c6b6343eb Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 31 Jul 2023 11:18:59 +0100 Subject: [PATCH 45/90] trying to make nix work again --- cabal.project | 2 +- default.nix | 2 +- nix/nixpkgs.nix | 2 +- nix/sources.json | 22 +++++++++++++++++----- nix/sources.nix | 2 +- numskull.cabal | 2 +- numskull.nix | 2 +- shell.nix | 2 +- test/Main.hs | 4 ---- test/Test/Numskull.hs | 3 ++- 10 files changed, 26 insertions(+), 17 deletions(-) delete mode 100644 test/Main.hs diff --git a/cabal.project b/cabal.project index ec23658..5ad8ca2 100644 --- a/cabal.project +++ b/cabal.project @@ -3,4 +3,4 @@ packages: . source-repository-package type: git location: https://github.com/cchalmers/dense.git - tag: 6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262 + tag: 6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262 \ No newline at end of file diff --git a/default.nix b/default.nix index 59d4cd8..052e731 100644 --- a/default.nix +++ b/default.nix @@ -1,2 +1,2 @@ { nixpkgs ? import nix/nixpkgs.nix {} }: -nixpkgs.pkgs.haskellPackages.callPackage ./numskull.nix { } +nixpkgs.pkgs.haskellPackages.callPackage ./numskull.nix { } \ No newline at end of file diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index 1e517cd..78da415 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -15,4 +15,4 @@ let overlay = _: pkgs: { }; }; in -import sources.nixpkgs { overlays = [ overlay ] ; config = {}; } +import sources.nixpkgs { overlays = [ overlay ] ; config = {}; } \ No newline at end of file diff --git a/nix/sources.json b/nix/sources.json index c0fd4fd..b2d77fa 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -11,6 +11,18 @@ "url": "https://github.com/clash-lang/clash-compiler/archive/fa01fd98799cd7c00cae44a9df847142410f1618.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, + "dense": { + "branch": "master", + "description": "dense Haskell library", + "homepage": "", + "owner": "cchalmers", + "repo": "dense", + "rev": "6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262", + "sha256": "1v8nx36f8yhifn8jcv24jjwm2msnhg8lx5wji28ci9vr0swq72ly", + "type": "tarball", + "url": "https://github.com/cchalmers/dense/archive/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, "niv": { "branch": "master", "description": "Easy dependency management for Nix projects", @@ -24,15 +36,15 @@ "url_template": "https://github.com///archive/.tar.gz" }, "nixpkgs": { - "branch": "nixos-20.09", + "branch": "nixos-23.05", "description": "Nix Packages collection", "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "896270d629efd47d14972e96f4fbb79fc9f45c80", - "sha256": "0xmjjayg19wm6cn88sh724mrsdj6mgrql6r3zc0g4x9bx4y342p7", + "rev": "8163a64662b43848802092d52015ef60777d6129", + "sha256": "12dlinlsmn9m4597bbyd9wjlsi5n1fd33kwrlf5v8n9357cciq54", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/896270d629efd47d14972e96f4fbb79fc9f45c80.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/8163a64662b43848802092d52015ef60777d6129.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "revealjs": { @@ -59,4 +71,4 @@ "url": "https://github.com/zenzike/yoda/archive/fb3517e6de9187ece52d3fd0485d5564ac2b0ae8.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } -} +} \ No newline at end of file diff --git a/nix/sources.nix b/nix/sources.nix index 7a9a2ae..1a3a74e 100644 --- a/nix/sources.nix +++ b/nix/sources.nix @@ -86,4 +86,4 @@ mapAttrs (_: spec: spec // { outPath = callFunctionWith spec (getFetcher spec) { }; } else spec - ) sources + ) sources \ No newline at end of file diff --git a/numskull.cabal b/numskull.cabal index 96ea3bc..f33b9a9 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -48,4 +48,4 @@ test-suite doctest , vector , split ghc-options: -Wall - default-language: Haskell2010 + default-language: Haskell2010 \ No newline at end of file diff --git a/numskull.nix b/numskull.nix index 8212149..9a92d88 100644 --- a/numskull.nix +++ b/numskull.nix @@ -10,4 +10,4 @@ mkDerivation { base doctest hspec QuickCheck split vector ]; license = lib.licenses.mit; -} +} \ No newline at end of file diff --git a/shell.nix b/shell.nix index c2e4e52..de66c7c 100644 --- a/shell.nix +++ b/shell.nix @@ -4,4 +4,4 @@ haskell-language-server ghcid.bin ]; -}) +}) \ No newline at end of file diff --git a/test/Main.hs b/test/Main.hs deleted file mode 100644 index 3e2059e..0000000 --- a/test/Main.hs +++ /dev/null @@ -1,4 +0,0 @@ -module Main (main) where - -main :: IO () -main = putStrLn "Test suite not yet implemented." diff --git a/test/Test/Numskull.hs b/test/Test/Numskull.hs index ab5590e..95abfac 100644 --- a/test/Test/Numskull.hs +++ b/test/Test/Numskull.hs @@ -15,13 +15,14 @@ spec = do it "works" $ N.fromList [3] [1,2,3::Int] == N.fromList [3] [1,2,3::Int] +{- describe "padShape" $ do focus . it "works on a less simple example" $ property $ \content (NonNegative extra) -> let n = toInteger $ length content in padShape (N.fromList [n] content) [n + extra] `shouldBe` N.fromList [n + extra] (content <> replicate (fromInteger extra) (0 :: Int)) - +-} --cabal test --test-show-details=streaming -- ghci -isrc -itest test/Test/Numskull.hs -- ghci> hspec spec From 41ce67864d72653851e972f8a44bbab696a9b9b7 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 31 Jul 2023 18:02:24 +0100 Subject: [PATCH 46/90] broadcasting --- src/Indexing.hs | 162 ++++++++++++++++++++++++++++++++++++++ src/Numskull.hs | 202 +++++++++++++----------------------------------- src/Typing.hs | 25 ++++++ 3 files changed, 240 insertions(+), 149 deletions(-) create mode 100644 src/Indexing.hs create mode 100644 src/Typing.hs diff --git a/src/Indexing.hs b/src/Indexing.hs new file mode 100644 index 0000000..235ec71 --- /dev/null +++ b/src/Indexing.hs @@ -0,0 +1,162 @@ +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE GADTs #-} + +module Indexing where + +import qualified Data.Vector.Storable as V +import Data.Vector.Storable (Vector) +import qualified Data.Map as M +import Type.Reflection + +import NdArray +import Typing +import qualified DType +import DType (DType) + +data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) + +-- * Indexing & Slicing +{- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, +we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the +shape of the array, [sx,sy,sz,...], as follows: + + i = x + y*sx + z*sx*sy + ... + + x = i/(1) % sx + y = i/(sx) % sy + z = i/(sx*sy) % sz + ... +-} + +generateIndicies :: [Integer] -> [[Integer]] +generateIndicies = foldr (\x xs -> [ (i:t) | i <- [0..(x-1)], t <- xs]) [[]] +-- foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] [2,3,2] + +mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) +mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) + where + twoDinds = generateIndicies sh + oneDkey = zip [0..] twoDinds + twoDkey = zip twoDinds [0..] + +-- Unsafe: Indexes the vector with the multi-index using a mapping +vecInd :: forall a . DType a => M.Map [Integer] Int -> Vector a -> [Integer] -> a +vecInd mapp v i = v V.! (mapp M.! i) +--vecInd mapp v i = case v =@= (undefined :: Vector a) of +-- Just HRefl -> v V.! (mapp M.! i) + +-- | Converts a shape and multi-index to a 1D index. +collapseInd :: [Integer] -> [Integer] -> Integer +collapseInd sh indicies = collapseRun (reverse$sh) (reverse$indicies) 1 + +-- Helper for collapseInd +collapseRun :: [Integer] -> [Integer] -> Integer -> Integer +collapseRun _ [] _ = 0 +collapseRun [] _ _ = 0 +collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) + +-- | Converts a shape and 1D index to a multi-index. +expandInd :: [Integer] -> Integer -> [Integer] +expandInd sh i = reverse $ expandRun (reverse$sh) i 1 + +-- Helper for expandInd +expandRun :: [Integer] -> Integer -> Integer -> [Integer] +expandRun [] _ _ = [] +expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) + where x = (i `div` runSize) `mod` s + +-- | Converts the multi-index for one shape to another +map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer +map1DIndex s r i = collapseInd r (expandInd s i) + +-- | Checks an index does not exceed the shape +validIndex :: NdArray -> [Integer] -> Bool +validIndex (NdArray s _) i = (length i == length s) && (and $ zipWith lessAbs i s) + where lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) + +{- | Takes a multi-dimensional index and returns the value in the NdArray at that position. +Indicies can be negative, where -1 is the row in that dimension. +If an index exceeds the size of its dimension, a value will still be returned, the identity +value for the array e.g. 0. To avoid this use !?. +-} +-- >>> m = fromListFlat [2,4,8 :: Int] +-- >>> m #! [1] :: Int +-- 4 +-- >>> m #! [50] :: Int +-- 0 +(#!) :: DType a => NdArray -> [Integer] -> a +(NdArray s v) #! i = case (NdArray s v) !? i of + Just val -> val + Nothing -> DType.addId :: DType a => a + +{- | The safer version of #! which returns Nothing if an index exceeds the shape bounds. -} +-- >>> m = fromListFlat [2,4,8 :: Int] +-- >>> m !? [1] :: Maybe Int +-- Just 4 +-- >>> m !? [50] :: Maybe Int +-- Nothing +(!?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a +(NdArray s v) !? i = + let + -- Converts any negative indicies to their equivalent positives + positives = zipWith positiveInd s i + flatInd = fromIntegral $ collapseInd s positives :: Int + in + -- The type comparison should always hold + if validIndex (NdArray s v) i then + case ty v `eqTypeRep` typeRep @(Vector a) of + Just HRefl -> Just (v V.! flatInd) :: Maybe a -- Indexing the vector + Nothing -> Nothing + else Nothing + +(#!+) :: NdArray -> [IndexRange] -> NdArray +(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) + where (m,_) = mapIndicies sh + +forceRange :: IndexRange -> (Integer, Integer) +forceRange (I i) = (i,i) +forceRange (R s t) = (s,t) + +positiveInd s i = if i < 0 then s+i else i + +--(!?+) :: NdArray -> [IndexRange] -> Maybe NdArray +--(!?+) = undefined + +slice :: [(Integer, Integer)] -> NdArray -> NdArray +slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) + where (m,_) = mapIndicies sh + +(!/) :: NdArray -> [(Integer, Integer)] -> NdArray +(!/) nd ss = slice ss nd + +-- helper +sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray +sliceWithMap _ _ [] nd = nd +sliceWithMap _ d _ (NdArray sh v) | d >= length sh = (NdArray sh v) +sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ + sliceDim s d m (NdArray sh v) + +-- inclusive, supports negatives +sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray +sliceDim (x,y) d m (NdArray sh v) = + if d >= length sh then error "Given dimension does not exist in array." + else NdArray + (if y' < x' then [] else shrinkNth d (y'-x'+1) sh) + (V.ifilter + (\i _ -> + let dimInd = (m M.! i) !! d + in x' <= dimInd && dimInd <= y') + v + ) + where + dimSize = sh !! d + (x', y') = (positiveInd dimSize x, positiveInd dimSize y) + + +-- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell +shrinkNth :: Ord a => Int -> a -> [a] -> [a] +shrinkNth _ _ [] = [] +shrinkNth n newVal (x:xs) + | n == 0 = if newVal < x then newVal:xs else x:xs + | otherwise = x:shrinkNth (n-1) newVal xs diff --git a/src/Numskull.hs b/src/Numskull.hs index 1c50f8a..b6127ca 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -12,6 +12,8 @@ import qualified DType import DType (DType) import MatrixForm import Serialisation +import Indexing +import Typing import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) @@ -24,26 +26,6 @@ import Data.List (sort, elemIndex) -- >>> import Numskull as N -- >>> import qualified Vector - --- * Typing Shorthand --- | typeOf synonym. -ty :: Typeable a => a -> TypeRep a -ty = typeOf - --- | eqTypeRep synonym, returning Just HRefl in the case of type equality. --- >>> case True =@= False of --- >>> Just HRefl -> putStrLn "Two Booleans will match" --- >>> Nothing -> putStrLn "Mismatching types" --- Two Booleans will match -(=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) -(=@=) v u = eqTypeRep (ty v) (ty u) - --- Helper asserting a type -(<-@) ::Typeable a => a -> TypeRep b -> b -(<-@) val t = case eqTypeRep t (ty val) of - Just HRefl -> val - _ -> error "Mismatching type." - instance Eq NdArray where -- | Arrays are equal if their elements and shape exactly match. (NdArray s v) == (NdArray r u) = (r == s) && @@ -165,135 +147,6 @@ zeros _ s = NdArray s zerovec zerovec = (V.replicate (size s) ident) :: DType a => Vector a --- * Indexing & Slicing -{- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, -we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the -shape of the array, [sx,sy,sz,...], as follows: - - i = x + y*sx + z*sx*sy + ... - - x = i/(1) % sx - y = i/(sx) % sy - z = i/(sx*sy) % sz - ... - --} -generateIndicies :: [Integer] -> [[Integer]] -generateIndicies = foldr (\x xs -> [ (i:t) | i <- [0..(x-1)], t <- xs]) [[]] --- foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] [2,3,2] - -mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) -mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) - where - twoDinds = generateIndicies sh - oneDkey = zip [0..] twoDinds - twoDkey = zip twoDinds [0..] - --- Unsafe: Indexes the vector with the multi-index using a mapping -vecInd :: forall a . DType a => M.Map [Integer] Int -> Vector a -> [Integer] -> a -vecInd mapp v i = v V.! (mapp M.! i) ---vecInd mapp v i = case v =@= (undefined :: Vector a) of --- Just HRefl -> v V.! (mapp M.! i) - --- | Converts a shape and multi-index to a 1D index. -collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd sh indicies = collapseRun (reverse$sh) (reverse$indicies) 1 - --- Helper for collapseInd -collapseRun :: [Integer] -> [Integer] -> Integer -> Integer -collapseRun _ [] _ = 0 -collapseRun [] _ _ = 0 -collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) - --- | Converts a shape and 1D index to a multi-index. -expandInd :: [Integer] -> Integer -> [Integer] -expandInd sh i = reverse $ expandRun (reverse$sh) i 1 - --- Helper for expandInd -expandRun :: [Integer] -> Integer -> Integer -> [Integer] -expandRun [] _ _ = [] -expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) - where x = (i `div` runSize) `mod` s - --- | Converts the multi-index for one shape to another -map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer -map1DIndex s r i = collapseInd r (expandInd s i) - --- | Checks an index does not exceed the shape -validIndex :: NdArray -> [Integer] -> Bool -validIndex (NdArray s _) i = (length i == length s) && (and $ zipWith lessAbs i s) - where lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) - -{- | Takes a multi-dimensional index and returns the value in the NdArray at that position. -Indicies can be negative, where -1 is the row in that dimension. -If an index exceeds the size of its dimension, a value will still be returned, the identity -value for the array e.g. 0. To avoid this use !?. --} --- >>> m = fromListFlat [2,4,8 :: Int] --- >>> m #! [1] :: Int --- 4 --- >>> m #! [50] :: Int --- 0 -(#!) :: DType a => NdArray -> [Integer] -> a -(NdArray s v) #! i = case (NdArray s v) !? i of - Just val -> val - Nothing -> DType.addId :: DType a => a - -{- | The safer version of #! which returns Nothing if an index exceeds the shape bounds. -} --- >>> m = fromListFlat [2,4,8 :: Int] --- >>> m !? [1] :: Maybe Int --- Just 4 --- >>> m !? [50] :: Maybe Int --- Nothing -(!?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a -(NdArray s v) !? i = - let - -- Converts any negative indicies to their equivalent positives - positives = zipWith (\x y -> if y < 0 then x+y else y) s i - flatInd = fromIntegral $ collapseInd s positives :: Int - in - -- The type comparison should always hold - if validIndex (NdArray s v) i then - case ty v `eqTypeRep` typeRep @(Vector a) of - Just HRefl -> Just (v V.! flatInd) :: Maybe a -- Indexing the vector - Nothing -> Nothing - else Nothing - -slice :: [(Integer, Integer)] -> NdArray -> NdArray -slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) - where (m,_) = mapIndicies sh - -(!/) :: NdArray -> [(Integer, Integer)] -> NdArray -(!/) nd ss = slice ss nd - --- helpser -sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray -sliceWithMap _ _ [] nd = nd -sliceWithMap _ d _ (NdArray sh v) | d >= length sh = (NdArray sh v) -sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ - sliceDim s d m (NdArray sh v) - --- inclusive -sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray -sliceDim (x,y) d m (NdArray s v) = - if d >= length s then error "Given dimension does not exist in array." - else NdArray - (if y < x then [] else shrinkNth d (y-x+1) s) - (V.ifilter - (\i _ -> - let dimInd = (m M.! i) !! d - in x <= dimInd && dimInd <= y) - v - ) - --- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell -shrinkNth :: Ord a => Int -> a -> [a] -> [a] -shrinkNth _ _ [] = [] -shrinkNth n newVal (x:xs) - | n == 0 = if newVal < x then newVal:xs else x:xs - | otherwise = x:shrinkNth (n-1) newVal xs - - -- * Pointwise Functions -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances @@ -475,6 +328,57 @@ padShape r (NdArray s v) = then NdArray r (V.unsafeUpdate_ nullVec newIndices v) else error "Cannot map to a smaller shape." +--broadcast :: forall a . DType a => (NdArray, NdArray) -> Maybe (Vector a, Vector a) +broadcast :: (NdArray, NdArray) -> Maybe (NdArray, NdArray) +broadcast ((NdArray s v), (NdArray r u)) = + let + (s',v',r',u') = broadcastDimensions s v r u + newshape = sequenceA $ zipWith (\x y -> if x == y || x == 1 || y == 1 + then Just (max x y) else Nothing) s' r' + in + case newshape of + Nothing -> Nothing + Just ns -> Just ( + NdArray ns $ padRepeats ns m s' v', --NdArray ns $ + NdArray ns $ padRepeats ns m r' u') -- NdArray ns $ + where m = (fst $ mapIndicies ns) + +-- makes the number of dimensions correct but not the size of the later 1s +broadcastDimensions :: (DType a, DType b) => + [Integer] -> Vector a -> [Integer] -> Vector b -> + ([Integer], Vector a, [Integer], Vector b) +broadcastDimensions s v r u + | sl == rl = (s,v, + r,u) + | sl > rl = (s,v, + sdiff ++ r, + V.concat $ replicate (fromIntegral $ product $ sdiff) u) + | sl < rl = (rdiff ++ s, + V.concat $ replicate (fromIntegral $ product $ rdiff) v, + r,u) + where + sl = length s + rl = length r + diff = Prelude.abs (sl - rl) + sdiff = take diff s + rdiff = take diff r + +padRepeats :: DType a => + [Integer] -> M.Map Int [Integer] -> [Integer] -> Vector a -> Vector a +padRepeats newshape oneDmap s v = + let (_, multiMap) = mapIndicies s + in V.generate (fromIntegral $ product newshape) (\i -> + let + multiI = oneDmap M.! i -- equivalent multi-index + multiWrap = zipWith mod multiI s -- wrap the index over dimensions of size 1 + flatWrap = multiMap M.! multiWrap -- collapse the index over the vector + in v V.! flatWrap) + +-- Concatenate a list of tensors into a single tensor. All input tensors must have the# +-- same shape, except for the dimension size of the axis to concatenate on. +concat :: [NdArray] -> Integer -> Maybe NdArray +concat = undefined + -- * Matrix Operations -- ROWS, COLUMNS & DIAGONALS diff --git a/src/Typing.hs b/src/Typing.hs new file mode 100644 index 0000000..5153f12 --- /dev/null +++ b/src/Typing.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE TypeOperators #-} + +module Typing where + +import Type.Reflection + +-- * Typing Shorthand +-- | typeOf synonym. +ty :: Typeable a => a -> TypeRep a +ty = typeOf + +-- | eqTypeRep synonym, returning Just HRefl in the case of type equality. +-- >>> case True =@= False of +-- >>> Just HRefl -> putStrLn "Two Booleans will match" +-- >>> Nothing -> putStrLn "Mismatching types" +-- Two Booleans will match +(=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) +(=@=) v u = eqTypeRep (ty v) (ty u) + +-- Helper asserting a type +(<-@) ::Typeable a => a -> TypeRep b -> b +(<-@) val t = case eqTypeRep t (ty val) of + Just HRefl -> val + _ -> error "Mismatching type." \ No newline at end of file From d0a19b7ebcebed6a7f15daddec77aea8e2b8bf40 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 1 Aug 2023 15:12:20 +0100 Subject: [PATCH 47/90] gathering & concatenating --- src/Numskull.hs | 96 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index b6127ca..9bb6353 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -20,7 +20,7 @@ import Data.Vector.Storable (Vector) import Type.Reflection import qualified Data.Map as M import Data.Maybe (fromJust) -import Data.List (sort, elemIndex) +import Data.List (sort, elemIndex, intersect) -- $setup -- >>> import Numskull as N @@ -374,10 +374,100 @@ padRepeats newshape oneDmap s v = flatWrap = multiMap M.! multiWrap -- collapse the index over the vector in v V.! flatWrap) +identifyCommon :: forall a . Eq a => [[a]] -> Maybe [(Int, a)] +identifyCommon [] = Nothing +identifyCommon (x : xs) = + let + n = length x + indexed = traverse (\y -> if length y == n then Just (zip [(0::Int)..] y) else Nothing) (x:xs) :: Maybe [[(Int, a)]] + common = fmap (foldr intersect (head $ fromJust indexed)) indexed + in + case common of + Just c | length c < n-1 -> Nothing + otherwise -> common + -- Concatenate a list of tensors into a single tensor. All input tensors must have the# -- same shape, except for the dimension size of the axis to concatenate on. -concat :: [NdArray] -> Integer -> Maybe NdArray -concat = undefined +concatenateAlong [] _ = Nothing +concatenateAlong [nd] _ = Just nd +concatenateAlong ((NdArray s v):nds) axis = + let + maybeVs = extractVectors ((NdArray s v):nds) (vecType v) + in + case maybeVs of + Nothing -> Nothing + Just vs -> + case concatAlongVec vs (map shape ((NdArray s v):nds)) axis of + Just (ns, c) -> Just $ NdArray ns c + Nothing -> Nothing + +extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] +extractVectors [] _ = Just [] +extractVectors ((NdArray s v) : nds) t = + case v =@= (undefined :: Vector a) of + Just HRefl -> + case extractVectors nds t of + Just vs -> Just (v:vs) + _ -> Nothing + Nothing -> Nothing + +--concatenateAlong nds axis = case identifyCommon shapes of +concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) +concatAlongVec vs shs axis = case identifyCommon shs of + Nothing -> Nothing + Just c -> + if 0 <= axis && axis < n + then if (length c == n) || (length c == n-1 && not (elem axis (map fst c))) + then + let + concatIndicies = map (!! axis) shs -- axis can actually be concatenated along + concatSize = sum concatIndicies + steps = [0] ++ scanl1 (+) concatIndicies + ranges = zip steps (drop 1 steps) + fullranges = map (\(x,y) -> [x..y-1]) ranges + numbered = M.fromList $ concat $ zipWith (\x y -> map (\z-> (z, y)) x) fullranges [0..] + newshape = replaceNth axis concatSize base + (oneDmap,_) = mapIndicies newshape + multiMaps = map (\x -> snd $ mapIndicies x) shs + in + --in Just newshape + + Just (newshape, V.generate (fromIntegral $ product newshape) (\i -> + let + multiI = oneDmap M.! i + array = numbered M.! (multiI !! axis) + arraySize = concatIndicies !! array + arrayMultiI = replaceNth axis (multiI !! axis - (steps !! array)) multiI + arrayFlatI = (multiMaps !! array) M.! arrayMultiI + in + --arrayFlatI + (vs !! array) V.! arrayFlatI <-@ typeRep @(a) + ) ) + + else Nothing + else Nothing + where + base = head shs + n = length $ base + +replaceNth n x l = take n l ++ [x] ++ drop (n+1) l + +ctest = concatAlongVec [V.fromList [1..6::Int], V.fromList [11..16::Int], V.fromList [101..108::Int]] [[2,3], [2,3], [2,4]] 1 +dtest = concatenateAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 + +gather :: NdArray -> [Integer] -> Integer -> NdArray +gather nd is axis = fromJust $ concatenateAlong (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) ax + where + ax = fromIntegral axis + sliceLead = replicate ax (0,-1) + --(m,_) = mapIndicies $ shape nd + +onnxex = fromMatrix $ A [ + A [B (1.0::Float), B 1.2, B 1.9], + A [B 2.3, B 3.4, B 3.9], + A [B 4.5, B 5.7, B 5.9]] + +etest = gather onnxex [0,2] 1 -- * Matrix Operations From af33a5e021e56cd819e13eccb419b237eab50194 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 1 Aug 2023 15:12:39 +0100 Subject: [PATCH 48/90] gathering & concatenating --- numskull.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numskull.cabal b/numskull.cabal index f33b9a9..5f10308 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -14,7 +14,7 @@ build-type: Simple library import library-deps - exposed-modules: Numskull, Serialisation, DType, MatrixForm + exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 From 3ee6c7c18e3f92b41d61de80aa5385d7a8623b25 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 1 Aug 2023 17:42:04 +0100 Subject: [PATCH 49/90] tidying & max/min --- src/Numskull.hs | 112 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 9bb6353..abc127c 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -92,6 +92,17 @@ ndType (NdArray _ v) = case v =@= (undefined :: Vector a) of vecType :: forall a . DType a => Vector a -> TypeRep a vecType _ = typeRep @a +-- | Convert a list of arrays to a list of vectors, provided they are all of the specified type. +extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] +extractVectors [] _ = Just [] +extractVectors ((NdArray s v) : nds) t = + case v =@= (undefined :: Vector a) of + Just HRefl -> + case extractVectors nds t of + Just vs -> Just (v:vs) + _ -> Nothing + Nothing -> Nothing + -- Todo: get the ident of the dtype from an nd array indentityElem = undefined @@ -146,7 +157,6 @@ zeros _ s = NdArray s zerovec ident = (DType.addId :: DType a => a) zerovec = (V.replicate (size s) ident) :: DType a => Vector a - -- * Pointwise Functions -- -- All the numpy-like functions not defined within the Eq, Ord or Num instances @@ -204,6 +214,15 @@ shiftleft = mapTransform (DType.shiftleft) shiftright :: NdArray -> NdArray shiftright = mapTransform (DType.shiftright) +origin :: forall a . DType a => NdArray -> a +origin (NdArray s v) = (v V.! 0) <-@ typeRep @a + +maxElem :: forall a . DType a => NdArray -> a +maxElem nd = foldrA max (origin nd) nd + +minElem :: forall a . DType a => NdArray -> a +minElem nd = foldrA min (origin nd) nd + ----- Two Arguments -- | The generic function for operating on two DType arrays with the same shape in an element-wise/pointwise way. @@ -374,6 +393,7 @@ padRepeats newshape oneDmap s v = flatWrap = multiMap M.! multiWrap -- collapse the index over the vector in v V.! flatWrap) +{- identifyCommon :: forall a . Eq a => [[a]] -> Maybe [(Int, a)] identifyCommon [] = Nothing identifyCommon (x : xs) = @@ -385,40 +405,35 @@ identifyCommon (x : xs) = case common of Just c | length c < n-1 -> Nothing otherwise -> common +-} -- Concatenate a list of tensors into a single tensor. All input tensors must have the# -- same shape, except for the dimension size of the axis to concatenate on. -concatenateAlong [] _ = Nothing -concatenateAlong [nd] _ = Just nd -concatenateAlong ((NdArray s v):nds) axis = - let - maybeVs = extractVectors ((NdArray s v):nds) (vecType v) - in - case maybeVs of - Nothing -> Nothing - Just vs -> - case concatAlongVec vs (map shape ((NdArray s v):nds)) axis of - Just (ns, c) -> Just $ NdArray ns c +concatAlong [] _ = Nothing +concatAlong [nd] _ = Just nd +concatAlong ((NdArray s v):nds) axis = + case extractVectors ((NdArray s v):nds) (vecType v) of + Nothing -> Nothing + Just vs -> + case concatAlongVec vs (map shape ((NdArray s v):nds)) axis of Nothing -> Nothing + Just (ns, c) -> Just $ NdArray ns c -extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] -extractVectors [] _ = Just [] -extractVectors ((NdArray s v) : nds) t = - case v =@= (undefined :: Vector a) of - Just HRefl -> - case extractVectors nds t of - Just vs -> Just (v:vs) - _ -> Nothing - Nothing -> Nothing - +{- Converts the dimension of each sub-array at the axis to a mapping from +an index along this axis in the new array to the sub array it corresponds to.-} +--plotArrays :: [Integer] -> +--plotArrays dims = +-- concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim]]) [0..] dims + --concatenateAlong nds axis = case identifyCommon shapes of +{- concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) concatAlongVec vs shs axis = case identifyCommon shs of Nothing -> Nothing Just c -> if 0 <= axis && axis < n then if (length c == n) || (length c == n-1 && not (elem axis (map fst c))) - then + then let concatIndicies = map (!! axis) shs -- axis can actually be concatenated along concatSize = sum concatIndicies @@ -431,7 +446,6 @@ concatAlongVec vs shs axis = case identifyCommon shs of multiMaps = map (\x -> snd $ mapIndicies x) shs in --in Just newshape - Just (newshape, V.generate (fromIntegral $ product newshape) (\i -> let multiI = oneDmap M.! i @@ -449,14 +463,60 @@ concatAlongVec vs shs axis = case identifyCommon shs of where base = head shs n = length $ base +-} + +concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) +concatAlongVec vs shs axis = + if not (checkShapeLengths shs) || not (checkAxis axis shs) then Nothing + else + let + axDim = axisDimensions axis shs + newshape = replaceNth axis (sum axDim) (head shs) + -- first is sub-array number, second is sub-array index + arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim + (newMultiInds, _) = mapIndicies newshape + subArrayMaps = map (\x -> snd $ mapIndicies x) shs + in + Just (newshape, + V.generate (length newMultiInds) (\i -> + let + multiI = newMultiInds M.! i + (arrayNo, arrayAxInd) = arrayPlot !! (fromIntegral $ multiI !! axis) + array = vs !! arrayNo + arrayMap = subArrayMaps !! arrayNo + arrayMultiI = replaceNth axis arrayAxInd multiI + in + --arrayNo + vecInd arrayMap array arrayMultiI <-@ typeRep @(a) + ) + ) replaceNth n x l = take n l ++ [x] ++ drop (n+1) l +-- same number of dimensions +checkShapeLengths :: [[Integer]] -> Bool +checkShapeLengths [] = False +checkShapeLengths shapes = (filter (\sh -> length sh /= baseLen) shapes) == [] + where baseLen = length $ head shapes + +-- dimensions are the same save perhaps the axis one +checkAxis :: Int -> [[Integer]] -> Bool +checkAxis _ [] = False +checkAxis axis shapes = + let + dropAxis = map (\sh -> take axis sh ++ drop (axis+1) sh) shapes + base = head dropAxis + in 0 <= axis && axis <= length base && (foldr intersect base dropAxis) == base + +-- gets the size of the dimension of the axis over all the shapes +axisDimensions :: Int -> [[Integer]] -> [Integer] +axisDimensions axis shapes = map (!! axis) shapes + ctest = concatAlongVec [V.fromList [1..6::Int], V.fromList [11..16::Int], V.fromList [101..108::Int]] [[2,3], [2,3], [2,4]] 1 -dtest = concatenateAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 +dtest = concatAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 gather :: NdArray -> [Integer] -> Integer -> NdArray -gather nd is axis = fromJust $ concatenateAlong (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) ax +gather nd is axis = fromJust $ concatAlong (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) ax where ax = fromIntegral axis sliceLead = replicate ax (0,-1) From 93b4a735817c3ada99554742d66d86640abd1451 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 2 Aug 2023 09:52:10 +0100 Subject: [PATCH 50/90] add to cabal --- numskull.cabal | 2 +- src/Numskull.hs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/numskull.cabal b/numskull.cabal index 5f10308..85643b5 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -14,7 +14,7 @@ build-type: Simple library import library-deps - exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing + exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing, NdArray -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 diff --git a/src/Numskull.hs b/src/Numskull.hs index abc127c..52c8fab 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -223,6 +223,11 @@ maxElem nd = foldrA max (origin nd) nd minElem :: forall a . DType a => NdArray -> a minElem nd = foldrA min (origin nd) nd +clip :: forall a . DType a => a -> a -> NdArray -> NdArray +clip min max (NdArray s v) = case v =@= (undefined :: Vector a) of + Just HRefl -> mapA (\x -> if x > max then max else if x < min then min else x) (NdArray s v) + _ -> error "Min and max types do not match array type." + ----- Two Arguments -- | The generic function for operating on two DType arrays with the same shape in an element-wise/pointwise way. From 38755e043a6616e1d6a4397a2253394dba481028 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 2 Aug 2023 12:16:38 +0100 Subject: [PATCH 51/90] export tidying --- numskull.cabal | 4 +- src/DType.hs | 32 +++++----- src/Indexing.hs | 1 + src/Numskull.hs | 146 +++++++++++++++++++++++++++++++++---------- src/Serialisation.hs | 4 +- 5 files changed, 136 insertions(+), 51 deletions(-) diff --git a/numskull.cabal b/numskull.cabal index 85643b5..b757437 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -35,7 +35,9 @@ test-suite test , hspec , QuickCheck , numskull - ghc-options: -Wall + --ghc-options: -Wall + -- -Wno-unused-top-binds + ghc-options: -Wno-unused-top-binds default-language: Haskell2010 test-suite doctest diff --git a/src/DType.hs b/src/DType.hs index 77d63ec..ff0f0b1 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -94,9 +94,9 @@ instance DType Int32 where ceil x = x floor x = x -- Trig - sin x = (round $ P.sin $ fromIntegral x) :: Int32 - cos x = (round $ P.sin $ fromIntegral x) :: Int32 - tan x = (round $ P.sin $ fromIntegral x) :: Int32 + sin x = (round $ P.sin $ fromIntegral @Int32 @Float x) :: Int32 + cos x = (round $ P.sin $ fromIntegral @Int32 @Float x) :: Int32 + tan x = (round $ P.sin $ fromIntegral @Int32 @Float x) :: Int32 -- Logical invert x = -x shiftleft x = x * 2 @@ -125,9 +125,9 @@ instance DType Int64 where ceil x = x floor x = x -- Trig - sin x = (round $ P.sin $ fromIntegral x) :: Int64 - cos x = (round $ P.sin $ fromIntegral x) :: Int64 - tan x = (round $ P.sin $ fromIntegral x) :: Int64 + sin x = (round $ P.sin $ fromIntegral @Int64 @Float x) :: Int64 + cos x = (round $ P.sin $ fromIntegral @Int64 @Float x) :: Int64 + tan x = (round $ P.sin $ fromIntegral @Int64 @Float x) :: Int64 -- Logical invert x = -x shiftleft x = x * 2 @@ -148,7 +148,7 @@ instance DType Float where power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y - mod x y = fromIntegral (xi `P.mod` yi):: Integer + mod x y = fromIntegral @Int @Integer (xi `P.mod` yi) where xi = P.floor x; yi = P.floor y abs = P.abs signum = P.signum @@ -178,7 +178,7 @@ instance DType Double where power x d = x ** d pow x y = x ** y log x y = logBase x y - mod x y = fromIntegral (xi `P.mod` yi):: Integer + mod x y = fromIntegral @Int @Integer (xi `P.mod` yi) :: Integer where xi = P.floor x; yi = P.floor y abs = P.abs signum = P.signum @@ -204,7 +204,7 @@ instance DType Bool where subtract x y = (x || y) && not (x && y) multiply x y = x && y divide x y = not (x && y) - div x y = 0 + div _ _ = 0 power _x _d = undefined pow x y = toEnum (fromEnum x ^ fromEnum y) log _x _y = undefined @@ -236,13 +236,13 @@ instance DType Char where -- Numeric add x y = chr $ ord x + ord y subtract x y = chr $ min 0 $ ord x + ord y - multiply x y = undefined - divide x y = undefined - div x y = undefined - power x d = undefined - pow x y = undefined - log x y = undefined - mod x y = undefined + multiply _ _ = undefined + divide _ _ = undefined + div _ _ = undefined + power _ _ = undefined + pow _ _ = undefined + log _ _ = undefined + mod _ _ = undefined abs = undefined signum c = if isAlpha c then if isUpper c then 'A' else 'a' else if isDigit c then '0' else c diff --git a/src/Indexing.hs b/src/Indexing.hs index 235ec71..31c8dd0 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -118,6 +118,7 @@ forceRange :: IndexRange -> (Integer, Integer) forceRange (I i) = (i,i) forceRange (R s t) = (s,t) +positiveInd :: (Ord a, Num a) => a -> a -> a positiveInd s i = if i < 0 then s+i else i --(!?+) :: NdArray -> [IndexRange] -> Maybe NdArray diff --git a/src/Numskull.hs b/src/Numskull.hs index 52c8fab..ac98464 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -5,13 +5,86 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE FlexibleContexts #-} -module Numskull where +module Numskull ( + -- Metadata + size + , shape + , ndType + + -- Creation + , fromList + , fromListFlat + , fromMatrix + , singleton + , arange + , zeros + + -- General mapping, folding & zipping + , foldrA + , mapA + , mapTransform + , pointwiseZip + + -- Summaries + , origin + , maxElem + , minElem + + -- Mathematical constant + , scale + , absA + , signumA + , ceilA + , floorA + , sinA + , cosA + , tanA + , invertA + , shiftleftA + , shiftrightA + + -- Mathematical pointwise + , elemDivide + , elemPow + + -- Bounds + , clip + + -- Type Conversions + , convertDTypeTo + , matchDType + + -- Size conversions + , constrainSize + , padSize + , setSize + , resize + + -- Shape conversions/manipulations + , padShape + , broadcast + , concatAlong + , gather + + -- Matrix manipulation + , swapRows + , diagonal + , transpose + , transposePerm + + --Matrix multiplication + , dot + , matMul + , upperTriangle + , determinant + , determinant2D + , swapRowsWith0Pivot +) where import NdArray import qualified DType import DType (DType) import MatrixForm -import Serialisation import Indexing import Typing @@ -95,7 +168,7 @@ vecType _ = typeRep @a -- | Convert a list of arrays to a list of vectors, provided they are all of the specified type. extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] extractVectors [] _ = Just [] -extractVectors ((NdArray s v) : nds) t = +extractVectors ((NdArray _ v) : nds) t = case v =@= (undefined :: Vector a) of Just HRefl -> case extractVectors nds t of @@ -104,7 +177,8 @@ extractVectors ((NdArray s v) : nds) t = Nothing -> Nothing -- Todo: get the ident of the dtype from an nd array -indentityElem = undefined +identityElem :: forall a . DType a => NdArray -> a +identityElem (NdArray s v) = indentityElem' v <-@ typeRep @a -- Helper for the vectors in identityElem indentityElem' :: forall a . DType a => Vector a -> a @@ -144,7 +218,10 @@ fromMatrix m = NdArray (matrixShape m) (V.fromList l) singleton :: DType a => a -> NdArray singleton x = NdArray [1] (V.fromList [x]) -arange = undefined +-- | Creates a flat array over the specified range. +arange :: (Enum a, DType a) => a -> a -> NdArray +arange mini maxi = if mini <= maxi then NdArray [fromIntegral $ fromEnum maxi - fromEnum mini] $ V.fromList [mini..maxi] + else error "Minimum of range is higher than maximum." {- | Creates the smallest possible square matrix from the given list, padding out any required space with the identity element for the DType -} @@ -184,35 +261,35 @@ mapTransform f (NdArray s v) = NdArray s (V.map f v) scale :: forall a . DType a => a -> NdArray -> NdArray scale x = mapA (DType.multiply x) -abs :: NdArray -> NdArray -abs = mapTransform (DType.abs) +absA :: NdArray -> NdArray +absA = mapTransform (DType.abs) -signum :: NdArray -> NdArray -signum = mapTransform (DType.signum) +signumA :: NdArray -> NdArray +signumA = mapTransform (DType.signum) -ceil :: NdArray -> NdArray -ceil = mapTransform (DType.ceil) +ceilA :: NdArray -> NdArray +ceilA = mapTransform (DType.ceil) -floor :: NdArray -> NdArray -floor = mapTransform (DType.floor) +floorA :: NdArray -> NdArray +floorA = mapTransform (DType.floor) -sin :: NdArray -> NdArray -sin = mapTransform (DType.sin) +sinA :: NdArray -> NdArray +sinA = mapTransform (DType.sin) -cos :: NdArray -> NdArray -cos = mapTransform (DType.cos) +cosA :: NdArray -> NdArray +cosA = mapTransform (DType.cos) -tan :: NdArray -> NdArray -tan = mapTransform (DType.tan) +tanA :: NdArray -> NdArray +tanA = mapTransform (DType.tan) -invert :: NdArray -> NdArray -invert = mapTransform (DType.invert) +invertA :: NdArray -> NdArray +invertA = mapTransform (DType.invert) -shiftleft :: NdArray -> NdArray -shiftleft = mapTransform (DType.shiftleft) +shiftleftA :: NdArray -> NdArray +shiftleftA = mapTransform (DType.shiftleft) -shiftright :: NdArray -> NdArray -shiftright = mapTransform (DType.shiftright) +shiftrightA :: NdArray -> NdArray +shiftrightA = mapTransform (DType.shiftright) origin :: forall a . DType a => NdArray -> a origin (NdArray s v) = (v V.! 0) <-@ typeRep @a @@ -224,8 +301,8 @@ minElem :: forall a . DType a => NdArray -> a minElem nd = foldrA min (origin nd) nd clip :: forall a . DType a => a -> a -> NdArray -> NdArray -clip min max (NdArray s v) = case v =@= (undefined :: Vector a) of - Just HRefl -> mapA (\x -> if x > max then max else if x < min then min else x) (NdArray s v) +clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of + Just HRefl -> mapA (\x -> if x > maxi then maxi else if x < mini then mini else x) (NdArray s v) _ -> error "Min and max types do not match array type." ----- Two Arguments @@ -273,7 +350,7 @@ convertDTypeTo t (NdArray s v) = convertDTFromTo (vecType v) t (NdArray s v) -- Helper with additional typing information convertDTFromTo :: forall a b . (DType a, DType b) => TypeRep a -> TypeRep b -> NdArray -> NdArray -convertDTFromTo t1 t2 (NdArray s v) = case v =@= (undefined :: Vector a) of +convertDTFromTo _ _ (NdArray s v) = case v =@= (undefined :: Vector a) of Just HRefl -> NdArray s (V.map convert v) _ -> error "Impossible type mismatch." where @@ -414,6 +491,7 @@ identifyCommon (x : xs) = -- Concatenate a list of tensors into a single tensor. All input tensors must have the# -- same shape, except for the dimension size of the axis to concatenate on. +concatAlong :: [NdArray] -> Int -> Maybe NdArray concatAlong [] _ = Nothing concatAlong [nd] _ = Just nd concatAlong ((NdArray s v):nds) axis = @@ -496,6 +574,7 @@ concatAlongVec vs shs axis = ) ) +replaceNth :: Int -> a -> [a] -> [a] replaceNth n x l = take n l ++ [x] ++ drop (n+1) l -- same number of dimensions @@ -517,8 +596,8 @@ checkAxis axis shapes = axisDimensions :: Int -> [[Integer]] -> [Integer] axisDimensions axis shapes = map (!! axis) shapes -ctest = concatAlongVec [V.fromList [1..6::Int], V.fromList [11..16::Int], V.fromList [101..108::Int]] [[2,3], [2,3], [2,4]] 1 -dtest = concatAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 +--ctest = concatAlongVec [V.fromList [1..6::Int], V.fromList [11..16::Int], V.fromList [101..108::Int]] [[2,3], [2,3], [2,4]] 1 +--dtest = concatAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 gather :: NdArray -> [Integer] -> Integer -> NdArray gather nd is axis = fromJust $ concatAlong (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) ax @@ -527,12 +606,14 @@ gather nd is axis = fromJust $ concatAlong (map (\i -> slice (sliceLead ++ [(i,i sliceLead = replicate ax (0,-1) --(m,_) = mapIndicies $ shape nd +{- onnxex = fromMatrix $ A [ A [B (1.0::Float), B 1.2, B 1.9], A [B 2.3, B 3.4, B 3.9], A [B 4.5, B 5.7, B 5.9]] etest = gather onnxex [0,2] 1 +-} -- * Matrix Operations @@ -638,16 +719,17 @@ matMulVec s v r u = -- element at position [i,j] in the resultant nxp matrix (from matMultiplying a prev: mxn and pxm = pxn) --matMulElem :: DType a => -- NdArray -> M.Map [Integer] Int -> NdArray -> M.Map [Integer] Int -> [Integer] -> a -matMulElem :: DType a => +matMulElem :: forall a . DType a => ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a matMulElem map1 map2 ks (i:j:_) = foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks - --sum [DType.multiply (nd1>![i,k]) (nd2>![k,j]) | [k <- 1..m]] +matMulElem _ _ _ _ = DType.multId :: a -- DETERMINANTS & INVERSES -- | Converts a nxn matrix to upper triangle form. O(n^3). upperTriangle :: NdArray -> NdArray +upperTriangle (NdArray [] v) = (NdArray [] v) upperTriangle (NdArray (c:rs) v) = let (_, fromMulti) = mapIndicies (c:rs) diff --git a/src/Serialisation.hs b/src/Serialisation.hs index c47aa3b..d14e68a 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -9,7 +9,7 @@ module Serialisation where import DType import NdArray -import Data.Int +import Data.Int() import System.IO import Data.List as List import Data.List.Split @@ -122,7 +122,7 @@ buffArray t h i = do _ -> error "Given TypeRep does not match data type." loadPayload :: forall a . DType a => Handle -> [Integer] -> TypeRep a -> IO NdArray -loadPayload h sh t = do +loadPayload h sh _ = do l <- traverse id $ buffArray (typeRep @a) h (product sh) pure $ NdArray sh (V.fromList l) From b8ee0a677c1d87527ce64d7d246cdeecaa70b77e Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 2 Aug 2023 17:10:01 +0100 Subject: [PATCH 52/90] add vector constructor --- src/NdArray.hs | 2 +- src/Numskull.hs | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/NdArray.hs b/src/NdArray.hs index ee87635..c0e4a16 100644 --- a/src/NdArray.hs +++ b/src/NdArray.hs @@ -14,4 +14,4 @@ data NdArray where -- Todo: show in a nicer shapely form :) instance Show NdArray where - show (NdArray s v) = show s <> " " <> show v \ No newline at end of file + show (NdArray s v) = "{elements: " <> show v <> ", shape: " <> show s <> "}" \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index ac98464..d1bd4b4 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -22,8 +22,9 @@ module Numskull ( -- General mapping, folding & zipping , foldrA , mapA - , mapTransform - , pointwiseZip + , mapTransform + , pointwiseZip + , pointwiseBool -- Summaries , origin @@ -92,7 +93,7 @@ import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import Type.Reflection import qualified Data.Map as M -import Data.Maybe (fromJust) +import Data.Maybe (fromJust, isJust) import Data.List (sort, elemIndex, intersect) -- $setup @@ -212,6 +213,13 @@ fromMatrix :: DType a => TreeMatrix a -> NdArray fromMatrix m = NdArray (matrixShape m) (V.fromList l) where l = flattenToList $ matrixToTree m +-- | The safe standard constructor. Returns Nothing if the +-- shape does not match the given vector length. +fromVector :: DType a => [Integer] -> Vector a -> Maybe NdArray +fromVector sh v = if V.length v == fromIntegral (product sh) + then Just $ NdArray sh v + else Nothing + -- | Creates a 1x1 matrix -- >>> printArray $ singleton (3::Int) -- 3 @@ -300,9 +308,21 @@ maxElem nd = foldrA max (origin nd) nd minElem :: forall a . DType a => NdArray -> a minElem nd = foldrA min (origin nd) nd -clip :: forall a . DType a => a -> a -> NdArray -> NdArray +bound :: Ord a => a -> a -> a -> a +bound mini maxi x + | x <= mini = mini + | x >= maxi = maxi + | otherwise = x + +-- NB must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd +clip :: forall a . DType a => Maybe a -> Maybe a -> NdArray -> NdArray clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of - Just HRefl -> mapA (\x -> if x > maxi then maxi else if x < mini then mini else x) (NdArray s v) + Just HRefl -> + case (mini, maxi) of + (Just mn, Just mx) -> mapA (\x -> bound mn mx x) (NdArray s v) + (Just mn, Nothing) -> mapA (\x -> bound mn x x) (NdArray s v) + (Nothing, Just mx) -> mapA (\x -> bound x mx x) (NdArray s v) + (Nothing, Nothing) -> (NdArray s v) _ -> error "Min and max types do not match array type." ----- Two Arguments @@ -320,6 +340,13 @@ pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then Nothing -> error $ typeMismatch (show$ty v) (show$ty u) else error $ shapeMismatch (show s) (show r) +pointwiseBool :: (forall t . DType t => t -> t -> Bool) -> NdArray -> NdArray -> NdArray +pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then + case v =@= u of + Just HRefl -> NdArray s (V.zipWith zipfunc v u) -- Types match + Nothing -> error $ typeMismatch (show$ty v) (show$ty u) + else error $ shapeMismatch (show s) (show r) + -- Todo: Needs to operate on doubles --elemDivide :: NdArray -> NdArray -> NdArray --elemDivide = pointwiseZip divide @@ -696,10 +723,10 @@ dot nd1 nd2 = foldrA (DType.add) (DType.addId) (nd1*nd2) matMul :: NdArray -> NdArray -> NdArray matMul (NdArray s v) (NdArray r u) = if (length s /= 2) || (length r /= 2) || s!!1 /= r!!0 then - error "Invalid matrix dimensions." + error "Matricies must be 2D and the number of columns in the first must match the number of rows in the second." else case v =@= u of Just HRefl -> NdArray sh (matMulVec s v r u) - _ -> error "Mismatching types" + _ -> error "Cannot multiply matricies of two distinct types." where sh = [s!!0, r!!1] From bda8f45c539a73d0251951d13aa6eb6c4ba5e133 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 2 Aug 2023 17:29:57 +0100 Subject: [PATCH 53/90] exporting more stuff --- src/Numskull.hs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index d1bd4b4..6f9d34b 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -7,14 +7,18 @@ module Numskull ( -- Metadata - size + DType + , size , shape , ndType -- Creation + , NdArray , fromList , fromListFlat + , TreeMatrix , fromMatrix + , fromVector , singleton , arange , zeros @@ -80,6 +84,21 @@ module Numskull ( , determinant , determinant2D , swapRowsWith0Pivot + + -- Indexing + , collapseInd + , expandInd + , map1DIndex + , validIndex + , (#!) + , (!?) + , (#!+) + , slice + , (!/) + + -- Pretty printing + , printArray + ) where import NdArray From 4ac4899b9dbbe0980ca82f8e4a0104fd2dffaec9 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 2 Aug 2023 19:05:11 +0100 Subject: [PATCH 54/90] more exports --- src/Numskull.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Numskull.hs b/src/Numskull.hs index 6f9d34b..32bca3d 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -14,6 +14,7 @@ module Numskull ( -- Creation , NdArray + , NdArray.NdArray , fromList , fromListFlat , TreeMatrix @@ -66,6 +67,7 @@ module Numskull ( , resize -- Shape conversions/manipulations + , reshape , padShape , broadcast , concatAlong From 396d7573ab73deadbbb8202e0450c83dc5a8b268 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 3 Aug 2023 09:21:08 +0100 Subject: [PATCH 55/90] tidying --- src/Numskull.hs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 32bca3d..e577865 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -10,6 +10,7 @@ module Numskull ( DType , size , shape + , getVector , ndType -- Creation @@ -537,12 +538,12 @@ identifyCommon (x : xs) = otherwise -> common -} --- Concatenate a list of tensors into a single tensor. All input tensors must have the# +-- Concatenate a list of tensors into a single tensor. All input tensors must have the -- same shape, except for the dimension size of the axis to concatenate on. -concatAlong :: [NdArray] -> Int -> Maybe NdArray -concatAlong [] _ = Nothing -concatAlong [nd] _ = Just nd -concatAlong ((NdArray s v):nds) axis = +concatAlong :: Int -> [NdArray] -> Maybe NdArray +concatAlong _ [] = Nothing +concatAlong _ [nd] = Just nd +concatAlong axis ((NdArray s v):nds) = case extractVectors ((NdArray s v):nds) (vecType v) of Nothing -> Nothing Just vs -> @@ -648,7 +649,7 @@ axisDimensions axis shapes = map (!! axis) shapes --dtest = concatAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 gather :: NdArray -> [Integer] -> Integer -> NdArray -gather nd is axis = fromJust $ concatAlong (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) ax +gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) where ax = fromIntegral axis sliceLead = replicate ax (0,-1) From e9dc3b311a8c714edbe8c2524efad676c9e50ed7 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 3 Aug 2023 10:15:02 +0100 Subject: [PATCH 56/90] ndtype string --- src/Numskull.hs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index e577865..512c551 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -178,12 +178,14 @@ shape (NdArray s _) = s getVector :: forall a . DType a => NdArray -> Vector a getVector (NdArray _ v) = v <-@ typeRep @(Vector a) --- | Gets the TypeRep for the NdArray elements -ndType :: forall a . DType a => NdArray -> TypeRep a -ndType (NdArray _ v) = case v =@= (undefined :: Vector a) of - Just HRefl -> vecType v :: TypeRep a +-- | Gets the TypeRep String representation the NdArray elements +ndType :: NdArray -> String +ndType (NdArray _ v) = show $ vecType v +{- +ndType (NdArray _ v) = case v =@= (undefined :: DType a => Vector a) of + Just HRefl -> show $ vecType v _ -> error "Impossible type mismatch." - +-} -- Helper to get the vector typeRep vecType :: forall a . DType a => Vector a -> TypeRep a vecType _ = typeRep @a From 761f6e23b9f3697abee3404c1732ed6c0c98b6f6 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 3 Aug 2023 17:01:17 +0100 Subject: [PATCH 57/90] working on gemm --- src/Numskull.hs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/Numskull.hs b/src/Numskull.hs index 512c551..2038d6f 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -87,6 +87,7 @@ module Numskull ( , determinant , determinant2D , swapRowsWith0Pivot + , gemm -- Indexing , collapseInd @@ -102,6 +103,9 @@ module Numskull ( -- Pretty printing , printArray + -- typing + , (=@=) + ) where import NdArray @@ -895,6 +899,82 @@ frontColumn col s v = V.ifilter rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) +-- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first +gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray +gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = + let + -- Apply transposition to A and B if specified + (sAT, vAT) = applyTransposition (sA, vA) transA + (sBT, vBT) = applyTransposition (sB, vB) transB + in + -- Check all the types match + case gemmTyping vAT vBT vC alpha beta of + Nothing -> Nothing + Just (vA', vB', vC', alpha', beta') -> + -- Check A and B have shapes (M,K) and (K, N) + if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing + else + let + alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + sAB = shape alphaAB + in + -- Check if C dimension matches or is broadcastable + if (sC!!0 /= 1 && sC!!0 /= sAB!!0) || (sC!!1 /= 1 && sC!!1 /= sAB!!1) then Nothing + else + let betaC = scale beta' $ if (sC!!0 /= sAB!!0) || (sC!!1 /= sAB!!1) + then snd $ fromJust $ broadcast (alphaAB, NdArray sC vC') + else (NdArray sC vC') + in + -- Finally, combine the two + Just $ alphaAB + betaC + +{- +Ok so we need to convert the scalars to whatever the matrix types are +and check matrix types all match +and check the a and b shapes are good +and possibly size c up +and scale the alphas & betas +-} + +applyTransposition :: DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) +applyTransposition (s, v) b = + if b then + let (NdArray sT vT) = transpose (NdArray s v) + in case vT =@= v of + Just HRefl -> (sT, vT) + _ -> error "Impossible type mismatch" + else (s, v) + +-- Checking all mats are same type & converting scalars if neccersary +gemmTyping :: forall a b c d e . (DType a, DType b, DType c, DType d, DType e) => + Vector a -> Vector b -> Vector c -> d -> e -> + Maybe (Vector a, Vector a, Vector a, a, a) +gemmTyping vA vB vC alpha beta = + case vA =@= vB of + Just HRefl -> + case vA =@= vC of + Just HRefl -> + -- All matricies match types + let + vA' = vA :: Vector a + vB' = vB :: Vector a + vC' = vC :: Vector a + + -- Convert scalar types + alpha' = + case alpha =@= (undefined :: a) of + Just HRefl -> alpha :: a + _ -> DType.rationalToDtype (DType.dtypeToRational alpha) :: a + beta' = + case beta =@= (undefined :: a) of + Just HRefl -> beta :: a + _ -> DType.rationalToDtype (DType.dtypeToRational beta) :: a + in + Just (vA', vB', vC', alpha', beta') + _ -> Nothing + _ -> Nothing + + -- * Common Errors shapeMismatch :: String -> String -> String shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." From 3db5841a5a6380478f2ce7127d8b0cef8b4c0ba5 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 3 Aug 2023 17:15:17 +0100 Subject: [PATCH 58/90] gemm compiles --- src/Numskull.hs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 2038d6f..dc6c2bc 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -908,7 +908,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = (sBT, vBT) = applyTransposition (sB, vB) transB in -- Check all the types match - case gemmTyping vAT vBT vC alpha beta of + case gemmTyping vAT vBT vC alpha beta of Nothing -> Nothing Just (vA', vB', vC', alpha', beta') -> -- Check A and B have shapes (M,K) and (K, N) @@ -936,15 +936,15 @@ and possibly size c up and scale the alphas & betas -} -applyTransposition :: DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) +applyTransposition :: forall a . DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) applyTransposition (s, v) b = - if b then - let (NdArray sT vT) = transpose (NdArray s v) - in case vT =@= v of - Just HRefl -> (sT, vT) - _ -> error "Impossible type mismatch" - else (s, v) - + let + ndT = Numskull.transpose (NdArray s v) + sT = shape ndT + vT = (getVector ndT) :: Vector a + in + if b then (sT, vT) else (s, v) + -- Checking all mats are same type & converting scalars if neccersary gemmTyping :: forall a b c d e . (DType a, DType b, DType c, DType d, DType e) => Vector a -> Vector b -> Vector c -> d -> e -> From 7b1ac6bc9a1e98271087042d7b9895c136207d44 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 3 Aug 2023 17:24:37 +0100 Subject: [PATCH 59/90] fixed transpose --- src/Numskull.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index dc6c2bc..4e2fbaf 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -714,7 +714,10 @@ diagonalVec s v = -- | Reverses the order of axes and switches the elements accordingly. transpose :: NdArray -> NdArray -transpose (NdArray sh v) = transposePerm [length sh -1..0] (NdArray sh v) +transpose (NdArray sh v) = transposePerm dec (NdArray sh v) + where + l = length sh + dec = [l-1, l-2 .. 0] -- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) transposePerm :: [Int] -> NdArray -> NdArray From 8dc0151b1275b4cdeccc2c0039e55209f049824f Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 11:02:50 +0100 Subject: [PATCH 60/90] trying dot with gemm --- src/Numskull.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 4e2fbaf..16bdb37 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -918,7 +918,8 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing else let - alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + --alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + alphaAB = scale alpha' (dot (NdArray sAT vA') (NdArray sBT vB')) sAB = shape alphaAB in -- Check if C dimension matches or is broadcastable From e1b708b8f6fd0cbf91f104b6c00a5cfef585e7a6 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 11:28:35 +0100 Subject: [PATCH 61/90] debug gemm info --- src/Numskull.hs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 16bdb37..4199cb0 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -122,6 +122,8 @@ import qualified Data.Map as M import Data.Maybe (fromJust, isJust) import Data.List (sort, elemIndex, intersect) +import Debug.Trace + -- $setup -- >>> import Numskull as N -- >>> import qualified Vector @@ -903,7 +905,9 @@ frontColumn col s v = V.ifilter columns = fromIntegral @Integer @Int $ s!!(length s -2) -- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first -gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray +gemm :: (DType a, DType b) => + NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> + Maybe (NdArray, NdArray, NdArray) gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = let -- Apply transposition to A and B if specified @@ -918,8 +922,8 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing else let - --alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) - alphaAB = scale alpha' (dot (NdArray sAT vA') (NdArray sBT vB')) + alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + --alphaAB = scale alpha' (dot (NdArray sAT vA') (NdArray sBT vB')) sAB = shape alphaAB in -- Check if C dimension matches or is broadcastable @@ -930,7 +934,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = else (NdArray sC vC') in -- Finally, combine the two - Just $ alphaAB + betaC + Just $ (alphaAB + betaC, alphaAB, betaC) {- Ok so we need to convert the scalars to whatever the matrix types are From 2ad2c15cdf95651b7f767f6ff71be50c5de04974 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 11:45:17 +0100 Subject: [PATCH 62/90] more gemm debug --- src/Numskull.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 4199cb0..ae16de2 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -907,7 +907,7 @@ frontColumn col s v = V.ifilter -- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> - Maybe (NdArray, NdArray, NdArray) + Maybe (NdArray, NdArray, NdArray, NdArray, NdArray, NdArray) gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = let -- Apply transposition to A and B if specified @@ -934,7 +934,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = else (NdArray sC vC') in -- Finally, combine the two - Just $ (alphaAB + betaC, alphaAB, betaC) + Just $ (alphaAB + betaC, alphaAB, betaC, (NdArray sAT vA'), (NdArray sBT vB'), matMul (NdArray sAT vA') (NdArray sBT vB')) {- Ok so we need to convert the scalars to whatever the matrix types are From 2f1b286e4227a7cffe12123ffc3ceab528266a4e Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 12:28:06 +0100 Subject: [PATCH 63/90] stumped --- src/Numskull.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index ae16de2..3a2251c 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -907,7 +907,7 @@ frontColumn col s v = V.ifilter -- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> - Maybe (NdArray, NdArray, NdArray, NdArray, NdArray, NdArray) + Maybe (NdArray, NdArray, NdArray) gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = let -- Apply transposition to A and B if specified @@ -934,7 +934,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = else (NdArray sC vC') in -- Finally, combine the two - Just $ (alphaAB + betaC, alphaAB, betaC, (NdArray sAT vA'), (NdArray sBT vB'), matMul (NdArray sAT vA') (NdArray sBT vB')) + Just $ (alphaAB + betaC, (NdArray sAT vA'), (NdArray sAT vAT)) {- Ok so we need to convert the scalars to whatever the matrix types are From bed922930824b7bd1920249120401ba8e02f55ec Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 12:57:24 +0100 Subject: [PATCH 64/90] urgh its concat thats causing issues :(( --- src/Numskull.hs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 3a2251c..85f515a 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -906,8 +906,7 @@ frontColumn col s v = V.ifilter -- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first gemm :: (DType a, DType b) => - NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> - Maybe (NdArray, NdArray, NdArray) + NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe (NdArray) gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = let -- Apply transposition to A and B if specified @@ -923,7 +922,6 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = else let alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) - --alphaAB = scale alpha' (dot (NdArray sAT vA') (NdArray sBT vB')) sAB = shape alphaAB in -- Check if C dimension matches or is broadcastable @@ -934,7 +932,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = else (NdArray sC vC') in -- Finally, combine the two - Just $ (alphaAB + betaC, (NdArray sAT vA'), (NdArray sAT vAT)) + Just $ (alphaAB + betaC) {- Ok so we need to convert the scalars to whatever the matrix types are From ec77ae6c9bc409fdecc032dc582dcb2d78055cd1 Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 4 Aug 2023 16:25:57 +0100 Subject: [PATCH 65/90] i think its finally ok --- src/Indexing.hs | 4 +- test/Test/aikens_out.txt | 195 +++++++++++++++++++++++++++++++++++++++ test/Test/rowans_out.txt | 194 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 test/Test/aikens_out.txt create mode 100644 test/Test/rowans_out.txt diff --git a/src/Indexing.hs b/src/Indexing.hs index 31c8dd0..541f335 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -124,6 +124,8 @@ positiveInd s i = if i < 0 then s+i else i --(!?+) :: NdArray -> [IndexRange] -> Maybe NdArray --(!?+) = undefined +{- | Takes a series of ranges corresponding to each dimension in the array and returns +the sub-array. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) where (m,_) = mapIndicies sh @@ -154,7 +156,7 @@ sliceDim (x,y) d m (NdArray sh v) = dimSize = sh !! d (x', y') = (positiveInd dimSize x, positiveInd dimSize y) - +-- Replaces the nth value of an array if the newValue is smaller. -- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell shrinkNth :: Ord a => Int -> a -> [a] -> [a] shrinkNth _ _ [] = [] diff --git a/test/Test/aikens_out.txt b/test/Test/aikens_out.txt new file mode 100644 index 0000000..5666a0a --- /dev/null +++ b/test/Test/aikens_out.txt @@ -0,0 +1,195 @@ +"Constant: IntScalar (Array V0 [3])" +"Input: FloatMatrix (Array (V2 500 12) [1.7640524,0.4001572,0.978738,2.2408931,1.867558,-0.9772779,0.9500884" +"Constant: IntScalar (Array V0 [9])" +"Gather: FloatMatrix (Array (V2 500 1) [0.41059852,0.6536186,-1.9807965,-0.4380743,0.3024719,5.1945396e-2,0.9" +"Constant: FloatMatrix (Array (V2 500 15) [5.0318122e-2,0.8964633,0.15310746,0.3642171,0.8965451,0.76894027,0.2" +"Constant: FloatMatrix (Array (V2 15 500) [4.968053e-2,0.57025206,0.45921576,0.14775777,0.25258988,0.44144374,0" +"Constant: IntScalar (Array V0 [0])" +"Gather: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" +"Constant: FloatMatrix (Array (V2 1 500) [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.117258" +/ +"Constant: IntScalar (Array V0 [2])" +"Gather: FloatMatrix (Array (V2 500 1) [0.978738,0.44386324,4.5758516e-2,-0.3873268,-0.89546657,-0.8131463,0." +"MatMul: Array (V2 1 1) [4.6461363]" +"Constant: FloatMatrix (Array (V2 500 1) [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.117258" +"MatMul: Array (V2 500 1) [3.0431468,3.7489762,4.526732,3.4945245,4.5740833,1.527907,0.5448006,9.191737e-2,2." +"Constant: FloatScalar (Array V0 [-1.0])" +"Constant: FloatMatrix (Array (V2 500 15) [0.28872812,0.22997248,0.82122374,0.21280658,9.241706e-2,5.1808298e-2" +"Constant: FloatMatrix (Array (V2 15 500) [5.8466375e-2,0.5863933,0.4988473,0.66705537,0.44786972,0.24729961,0." +"Constant: IntScalar (Array V0 [1])" +/ +"Gather: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" +"Constant: FloatMatrix (Array (V2 1 500) [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0." +"MatMul: Array (V2 1 1) [-12.392535]" +"Constant: FloatMatrix (Array (V2 500 1) [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0." +"MatMul: Array (V2 1 1) [-3.6630301]" +"MatMul: Array (V2 500 1) [-0.31687784,-0.55377394,-3.2807317,-0.9286113,-0.2849118,-1.044107,-0.73757833,-2." +"Identity: FloatScalar (Array V0 [-1.0])" +"Constant: FloatScalar (Array V0 [1.0])" +"MatMul: Array (V2 15 1) [-19.652634,-12.657694,-11.926977,-8.681829,-17.47995,-7.395108,-10.788708,-23.06547" +/ +"MatMul: Array (V2 500 1) [-75.00322,-115.5041,-96.75044,-84.42763,-76.216934,-101.877625,-122.852615,-90.928" +"Greater: BoolMatrix (Array (V2 500 1) [False,False,False,False,False,False,False,False,False,False,False,Fals" +"Cast: FloatMatrix (Array (V2 500 1) [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0" +"Mul: FloatMatrix (Array (V2 500 1) [-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0" +"LessOrEqual: BoolMatrix (Array (V2 500 1) [True,True,True,True,True,True,True,True,True,True,True,True,True,True," +"Cast: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1" +!!!! +"Mul: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" +"Add: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" +"Clip: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.0,1.0,-0.21274029,-0.35955316,-1.0,1.0,1.0,1" + +"Constant: IntScalar (Array V0 [4])" +"Gather: FloatMatrix (Array (V2 500 1) [1.867558,1.4940791,1.5327792,-1.048553,-0.51080513,0.17742614,-0.8707" +"MatMul: Array (V2 1 1) [-5.5109644]" +"MatMul: Array (V2 500 1) [-3.6095958,-4.446808,-5.369334,-4.1449924,-5.4254994,-1.8123103,-0.6462093,-0.1090" +"Constant: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [-3.0])" +"Constant: FloatMatrix (Array (V2 500 1) [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.3589" +"Constant: FloatMatrix (Array (V2 1 500) [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.3589" +"MatMul: Array (V2 1 1) [7.183905]" +/ +"MatMul: Array (V2 500 1) [0.91656965,6.203072,5.8669558,4.4492354,5.271462,5.4725223,2.5784698,3.1083322,6.9" +"Identity: FloatScalar (Array V0 [-1.0])" +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [0.91656965,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1." +"MatMul: Array (V2 15 1) [-20.942812,-3.784609,-6.334184,-16.134716,-18.233606,-8.202613,-9.120655,-9.406257," +"MatMul: Array (V2 500 1) [-68.49755,-113.25019,-92.551636,-71.3755,-91.53658,-80.38321,-96.32032,-59.690422," +"LessOrEqual: BoolMatrix (Array (V2 500 1) [True,True,True,True,True,True,True,True,True,True,True,True,True,True," +"Cast: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1" +"Mul: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" + +"Greater: BoolMatrix (Array (V2 500 1) [False,False,False,False,False,False,False,False,False,False,False,Fals" +"Cast: FloatMatrix (Array (V2 500 1) [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0" +"Mul: FloatMatrix (Array (V2 500 1) [-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0" +"Add: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" +"Constant: FloatScalar (Array V0 [-5.0])" +"Identity: FloatScalar (Array V0 [-5.0])" +"Constant: FloatMatrix (Array (V2 500 5) [0.23994845,0.874964,0.63754064,0.72970045,0.88023925,0.37286758,0.826" +"Constant: FloatMatrix (Array (V2 5 500) [0.68415046,0.64249843,0.38679576,0.2793504,0.33298904,0.8958253,0.164" +"MatMul: Array (V2 5 1) [9.695124,8.50157,12.497513,1.4205827,0.6591711]" + +"MatMul: Array (V2 500 1) [19.3494,21.16829,15.6027155,12.495917,17.27474,21.631218,21.864082,16.089157,18.32" +"Constant: FloatScalar (Array V0 [5.0])" +"Identity: FloatScalar (Array V0 [5.0])" +"Clip: FloatMatrix (Array (V2 500 1) [5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5" +"Clip: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" +"Mul: FloatMatrix (Array (V2 500 1) [3.29447,1.1370505,3.4790328,-1.290025,0.82438725,-0.11931206,-0.99218" +"Mul: FloatMatrix (Array (V2 500 1) [0.7473168,0.1817921,-1.5327792,-1.048553,0.10866883,-6.379413e-2,0.87" +"Constant: FloatMatrix (Array (V2 500 15) [0.49574316,0.7819777,0.69376004,0.1799764,0.5237755,0.5874824,0.8836" +"Constant: FloatMatrix (Array (V2 15 500) [0.32731897,3.096807e-2,0.28492284,0.24839026,0.16009456,0.795056,2.3" + +"MatMul: Array (V2 15 1) [6.6918817,-2.6588368,0.11027944,4.9703884,2.4028583,3.8567388,-7.34609,-6.4442456e-" +"MatMul: Array (V2 500 1) [6.801699,18.984343,8.779206,-0.84524286,4.8614564,7.481981,9.997283,9.963657,-4.68" +"Identity: FloatScalar (Array V0 [-1.0])" +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,-0.84524286,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Mul: FloatMatrix (Array (V2 500 1) [1.867558,1.4940791,1.5327792,0.8862819,-0.51080513,0.17742614,-0.8707" +"Constant: FloatMatrix (Array (V2 500 15) [0.44727606,0.35552752,0.2695884,0.9328208,0.48338705,0.5175878,0.272" +"Constant: FloatMatrix (Array (V2 15 500) [0.3689707,0.58033675,0.2554481,0.8095724,0.3360446,0.3746431,0.60134" +"Gather: FloatMatrix (Array (V2 500 1) [2.2408931,0.33367434,-0.18718386,-0.30230275,0.3869025,-1.7262826,-0." + +"MatMul: Array (V2 15 1) [14.817481,24.485401,20.55075,10.031889,20.104307,8.246814,5.722944,10.099542,5.8898" +"MatMul: Array (V2 500 1) [102.732124,108.530594,84.816505,91.67203,97.957214,92.46261,140.92859,100.8252,126" +"Constant: FloatScalar (Array V0 [-10.0])" +"Constant: FloatScalar (Array V0 [10.0])" +"Clip: FloatMatrix (Array (V2 500 1) [10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0" +"Mul: FloatMatrix (Array (V2 500 1) [18.67558,14.940791,15.327792,-10.48553,-5.1080513,1.7742615,-8.707972" +"Constant: FloatMatrix (Array (V2 500 5) [0.11452955,0.32926255,0.7802916,0.4259823,0.7317075,0.8767155,0.69868" +"Constant: FloatMatrix (Array (V2 5 500) [0.23298955,7.8250706e-2,0.30304825,0.20618552,6.613237e-2,0.8574571,0" +"MatMul: Array (V2 5 1) [-11.041234,-14.194939,-2.4407754,-8.023029,-7.5566373]" + +"MatMul: Array (V2 500 1) [-16.789845,-32.091545,-12.810639,-20.871101,-27.662287,-28.113258,-24.279068,-25.5" +"Constant: FloatScalar (Array V0 [-0.5])" +"Constant: FloatScalar (Array V0 [0.5])" +"Clip: FloatMatrix (Array (V2 500 1) [-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5" +"Constant: FloatMatrix (Array (V2 500 5) [0.34180975,0.98490405,0.20374298,0.3445685,0.60249984,0.9405572,7.452" +"Constant: FloatMatrix (Array (V2 5 500) [0.586107,0.99137485,0.11356729,0.5807301,4.3787777e-2,0.8292314,0.730" +"MatMul: Array (V2 5 1) [-15.098518,-11.611204,-3.7695518,-0.9945117,-9.389298]" +"MatMul: Array (V2 500 1) [-23.364492,-25.763008,-21.12878,-25.39899,-20.12619,-16.109161,-18.095474,-16.7098" +"Constant: FloatScalar (Array V0 [-1.2])" + +"Constant: FloatScalar (Array V0 [1.2])" +"Clip: FloatMatrix (Array (V2 500 1) [-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2" +"MatMul: Array (V2 1 1) [-16.21508]" +"MatMul: Array (V2 500 1) [-2.068826,-14.001203,-13.24254,-10.042547,-11.898429,-12.35225,-5.8199673,-7.01594" +"Constant: FloatScalar (Array V0 [3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Identity: FloatScalar (Array V0 [-3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-3.0,-1.8123103,-0.6462093,-0.109026805,-3.0,-2.8" +"Mul: FloatMatrix (Array (V2 500 1) [-5.602674,-4.4822373,-4.5983377,3.145659,1.5324154,-0.32155123,0.5627" + +"Clip: FloatMatrix (Array (V2 500 1) [-2.068826,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0" +"Constant: IntScalar (Array V0 [5])" +"Gather: FloatMatrix (Array (V2 500 1) [-0.9772779,-0.20515826,1.4693588,-1.420018,-1.1806322,-0.40178093,-0." +"Constant: FloatMatrix (Array (V2 500 1) [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.66" +"Constant: FloatMatrix (Array (V2 1 500) [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.66" +"MatMul: Array (V2 1 1) [-12.646031]" +"MatMul: Array (V2 500 1) [-4.2586102,-11.211211,-8.299928,-4.0727386,-1.4304522,-3.8753371,-8.4568405,-9.278" +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-1.4304522,-3.0,-3.0,-3.0,-3.0,-1.7071048,-3.0,-3" +"Mul: FloatMatrix (Array (V2 500 1) [2.021818,0.6154748,-4.4080763,4.2600536,3.5418968,1.2053428,1.736549," +"MatMul: Array (V2 1 1) [-8.988207]" +"MatMul: Array (V2 500 1) [-3.0268207,-7.968404,-5.8991995,-2.8947117,-1.0166985,-2.7544081,-6.010726,-6.5947" +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-2.8947117,-1.0166985,-2.7544081,-3.0,-3.0,-3.0,-1.213" +"Mul: FloatMatrix (Array (V2 500 1) [2.9318337,0.6154748,-4.4080763,4.110543,1.200347,1.1066687,1.736549,-" +"Constant: FloatMatrix (Array (V2 500 1) [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62" +"Constant: FloatMatrix (Array (V2 1 500) [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62" +"MatMul: Array (V2 1 1) [-7.2386208]" +"MatMul: Array (V2 500 1) [-4.4753776,-5.4445796,-3.7490675,-0.9543859,-4.7584944,-0.6636876,-4.501772,-1.904" +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-0.9543859,-3.0,-0.6636876,-3.0,-1.9042522,-3.0,-0.550" +"MatMul: Array (V2 1 1) [-1.5463191]" +"MatMul: Array (V2 500 1) [-0.95603323,-1.1630749,-0.8008784,-0.20387655,-1.0165128,-0.1417774,-0.9616716,-0." +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-0.95603323,-1.1630749,-0.8008784,-0.20387655,-1.0165128,-0.1417774,-" +"Mul: FloatMatrix (Array (V2 500 1) [0.93431014,0.23861441,-1.1767777,0.28950837,1.2001277,5.6963455e-2,0." +"Mul: FloatMatrix (Array (V2 500 1) [2.9318337,0.6154748,-4.4080763,4.2600536,3.5418968,0.72815174,0.37405" +"MatMul: Array (V2 1 1) [-12.180219]" +"MatMul: Array (V2 500 1) [-1.554032,-10.517229,-9.947348,-7.543621,-8.937696,-9.278591,-4.3717623,-5.2701373" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-1.554032,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-2.8" +"MatMul: Array (V2 1 1) [-15.176804]" +"MatMul: Array (V2 500 1) [-9.94057,-12.246192,-14.786765,-11.415014,-14.941439,-4.990974,-1.7796144,-0.30025" +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-1.7796144,-0.30025205,-3.0,-7.8356236e" +"MatMul: Array (V2 500 1) [-1.0720413,-1.8734934,-11.099167,-3.1416197,-0.9638959,-3.5323575,-2.495329,-8.631" +"Identity: FloatScalar (Array V0 [-3.0])" +"Identity: FloatScalar (Array V0 [3.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-1.0720413,-1.8734934,-3.0,-3.0,-0.9638959,-3.0,-2.495329,-3.0,-3.0,-" +"MatMul: Array (V2 1 1) [-11.002842]" +"MatMul: Array (V2 500 1) [-3.7052586,-9.754458,-7.2214584,-3.5435383,-1.2445834,-3.3717868,-7.3579826,-8.072" +"Identity: FloatScalar (Array V0 [-1.0])" +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0" +"MatMul: Array (V2 1 1) [12.541454]" +"MatMul: Array (V2 500 1) [7.7539277,9.433143,6.495541,1.6535453,8.244449,1.1498886,7.799658,3.2992601,7.7421" +"Identity: FloatScalar (Array V0 [-1.0])" +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.95448756,1.0,0.16623473,1.0,1.0" +"Identity: FloatScalar (Array V0 [-1.0])" +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,0.5448006,9.191737e-2,1.0,2.3987513e-2,1.0,1." +"Identity: FloatScalar (Array V0 [1.0])" +"Clip: FloatMatrix (Array (V2 500 1) [-0.31687784,-0.55377394,-1.0,-0.9286113,-0.2849118,-1.0,-0.73757833,-" +"Constant: IntScalar (Array V0 [6])" +"Gather: FloatMatrix (Array (V2 500 1) [0.95008844,0.3130677,0.15494743,-1.7062702,-2.8182229e-2,-1.6301984,-" +"Constant: IntScalar (Array V0 [7])" +"Gather: FloatMatrix (Array (V2 500 1) [-0.1513572,-0.85409576,0.37816253,1.9507754,0.42833188,0.46278226,5.6" +"Constant: IntScalar (Array V0 [8])" +"Gather: FloatMatrix (Array (V2 500 1) [-0.10321885,-2.5529897,-0.88778573,-0.5096522,6.651722e-2,-0.9072984," +"Constant: IntScalar (Array V0 [10])" +"Gather: FloatMatrix (Array (V2 500 1) [0.14404356,0.8644362,-0.34791216,-1.2527953,-0.6343221,0.7290906,0.46" +"Constant: IntScalar (Array V0 [11])" +"Gather: FloatMatrix (Array (V2 500 1) [1.4542735,-0.742165,0.15634897,0.7774904,-0.36274117,0.12898292,-1.53" +"Concat: FloatMatrix (Array (V2 500 28) [3.29447,0.7473168,1.867558,18.67558,-0.5,-1.2,5.0,2.021818,2.9318337" +"Constant: FloatMatrix (Array (V2 1 28) [-0.18432815,-3.3549756e-2,0.1182383,-8.149214e-2,-0.17400299,3.5006218" +"Constant: FloatMatrix (Array (V2 1 1) [6.1723504e-2])" +"Gemm: FloatMatrix (Array (V2 500 1) [-2.557051,-1.4029355,-1.2166657,2.4757802,0.6224911,0.714476,2.072814" +"Input: FloatMatrix (Array (V2 500 1) [-2.557051,-1.4029355,-1.2166657,2.4757802,0.6224911,0.714476,2.072814" \ No newline at end of file diff --git a/test/Test/rowans_out.txt b/test/Test/rowans_out.txt new file mode 100644 index 0000000..8263ffa --- /dev/null +++ b/test/Test/rowans_out.txt @@ -0,0 +1,194 @@ +"Constant: {elements: [3], shape: [1]}" +"Input: {elements: [1.7640524,0.4001572,0.978738,2.2408931,1.867558,-0.9772779,0.95008844,-0.1513572,-0.1032" +"Constant: {elements: [9], shape: [1]}" +"Gather: {elements: [0.41059852,0.6536186,-1.9807965,-0.4380743,0.3024719,5.1945396e-2,0.9008265,0.97663903,1" +"Constant: {elements: [5.0318122e-2,0.8964633,0.15310746,0.3642171,0.8965451,0.76894027,0.23978919,0.1712786,0." +"Constant: {elements: [4.968053e-2,0.57025206,0.45921576,0.14775777,0.25258988,0.44144374,0.826559,0.44754368,0" +"Constant: {elements: [0], shape: [1]}" +"Gather: {elements: [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394007,1.4882522,1.050" +"Constant: {elements: [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.11725885,1.9783616e-2,0.5" +/ +"Constant: {elements: [2], shape: [1]}" +"Gather: {elements: [0.978738,0.44386324,4.5758516e-2,-0.3873268,-0.89546657,-0.8131463,0.40234163,1.1787796," +"MatMul: {elements: [4.646127], shape: [1,1]}" +"Constant: {elements: [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.11725885,1.9783616e-2,0.5" +"MatMul: {elements: [3.043141,3.748969,4.5267234,3.4945176,4.5740743,1.527904,0.5447995,9.1917194e-2,2.772392" +"Constant: {elements: [-1.0], shape: []}" +"Constant: {elements: [0.28872812,0.22997248,0.82122374,0.21280658,9.241706e-2,5.1808298e-2,0.69124675,0.552448" +"Constant: {elements: [5.8466375e-2,0.5863933,0.4988473,0.66705537,0.44786972,0.24729961,0.5134718,0.24591672,5" +"Constant: {elements: [1], shape: [1]}" + +"Gather: {elements: [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1.2348258,1.8958892," +"Constant: {elements: [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0.20135742,0.69653153" +"MatMul: {elements: [-12.392532], shape: [1,1]}" +"Constant: {elements: [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0.20135742,0.69653153" +"MatMul: {elements: [-3.6630313], shape: [1,1]}" +"MatMul: {elements: [-0.31687793,-0.5537741,-3.2807329,-0.9286116,-0.28491193,-1.0441072,-0.7375786,-2.551416" +"Identity: {elements: [-1.0], shape: []}" +"Constant: {elements: [1.0], shape: []}" +"MatMul: {elements: [-19.65264,-12.657692,-11.92697,-8.681826,-17.479946,-7.3951087,-10.7887125,-23.065464,-4" + +"MatMul: {elements: [-75.00323,-115.504105,-96.75044,-84.427635,-76.21692,-101.87762,-122.85262,-90.92825,-10" +"Greater: {elements: [False,False,False,False,False,False,False,False,False,False,False,False,False,False,Fals" +"Cast: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," +"Mul: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," +"LessOrEqual: {elements: [True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,Tru" +"Cast: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +!!!! +"Mul: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Add: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," + +"Constant: {elements: [4], shape: [1]}" +"Gather: {elements: [1.867558,1.4940791,1.5327792,-1.048553,-0.51080513,0.17742614,-0.87079716,-1.0707526,1.8" +"MatMul: {elements: [-5.5109715], shape: [1,1]}" +"MatMul: {elements: [-3.6096005,-4.4468136,-5.3693414,-4.1449976,-5.4255066,-1.8123127,-0.6462102,-0.10902694" +"Constant: {elements: [-3.0], shape: []}" +"Identity: {elements: [-3.0], shape: []}" +"Constant: {elements: [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.35892314,0.43268,0.9716" +"Constant: {elements: [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.35892314,0.43268,0.9716" +"MatMul: {elements: [7.1839004], shape: [1,1]}" + +"MatMul: {elements: [0.916569,6.203068,5.8669515,4.449232,5.2714586,5.472519,2.578468,3.10833,6.9805555,6.504" +"Identity: {elements: [-1.0], shape: []}" +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [0.916569,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.5" +"MatMul: {elements: [-20.942808,-3.7846112,-6.33419,-16.134705,-18.2336,-8.202617,-9.120651,-9.4062605,-18.40" +"MatMul: {elements: [-68.49754,-113.25018,-92.55163,-71.37552,-91.5366,-80.383194,-96.32033,-59.690426,-74.75" +"LessOrEqual: {elements: [True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,Tru" +"Cast: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Mul: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," + +"Greater: {elements: [False,False,False,False,False,False,False,False,False,False,False,False,False,False,Fals" +"Cast: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," +"Mul: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," +"Add: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Constant: {elements: [-5.0], shape: []}" +"Identity: {elements: [-5.0], shape: []}" +"Constant: {elements: [0.23994845,0.874964,0.63754064,0.72970045,0.88023925,0.37286758,0.8268966,0.74059576,0.8" +"Constant: {elements: [0.68415046,0.64249843,0.38679576,0.2793504,0.33298904,0.8958253,0.1640203,0.12165266,0.7" +"MatMul: {elements: [9.695128,8.501572,12.497501,1.4205788,0.6591735], shape: [5,1]}" + +"MatMul: {elements: [19.349392,21.168283,15.602718,12.49592,17.27473,21.631216,21.864082,16.089157,18.328959," +"Constant: {elements: [5.0], shape: []}" +"Identity: {elements: [5.0], shape: []}" +"Clip: {elements: [5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0," +"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," +"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" +"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" +"Constant: {elements: [0.49574316,0.7819777,0.69376004,0.1799764,0.5237755,0.5874824,0.8836009,0.23305726,0.409" +"Constant: {elements: [0.32731897,3.096807e-2,0.28492284,0.24839026,0.16009456,0.795056,2.3362875e-2,0.9850323," + +"MatMul: {elements: [6.6918926,-2.6588376,0.11027652,4.97039,2.4028609,3.856735,-7.346094,-6.444438e-2,-4.226" +"MatMul: {elements: [6.8017015,18.984348,8.779214,-0.845247,4.861458,7.481983,9.9972925,9.963661,-4.686076,5." +"Identity: {elements: [-1.0], shape: []}" +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [1.0,1.0,1.0,-0.845247,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.16868544,1.0,1." +"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" +"Constant: {elements: [0.44727606,0.35552752,0.2695884,0.9328208,0.48338705,0.5175878,0.2729408,0.5210254,5.075" +"Constant: {elements: [0.3689707,0.58033675,0.2554481,0.8095724,0.3360446,0.3746431,0.6013461,0.58739585,0.9831" +"Gather: {elements: [2.2408931,0.33367434,-0.18718386,-0.30230275,0.3869025,-1.7262826,-0.6848101,-0.17992483" +"MatMul: {elements: [14.817481,24.48539,20.550753,10.031887,20.104303,8.246808,5.7229443,10.099546,5.8898077," +"MatMul: {elements: [102.7321,108.53057,84.81648,91.672005,97.9572,92.46259,140.92854,100.82519,126.107925,13" +"Constant: {elements: [-10.0], shape: []}" +"Constant: {elements: [10.0], shape: []}" +"Clip: {elements: [10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10." +"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" +"Constant: {elements: [0.11452955,0.32926255,0.7802916,0.4259823,0.7317075,0.8767155,0.6986809,0.8781359,0.6030" +"Constant: {elements: [0.23298955,7.8250706e-2,0.30304825,0.20618552,6.613237e-2,0.8574571,0.9918714,0.53765047" +"MatMul: {elements: [-11.04123,-14.194946,-2.440776,-8.023025,-7.5566387], shape: [5,1]}" +"MatMul: {elements: [-16.789845,-32.091545,-12.81064,-20.8711,-27.662294,-28.113256,-24.279064,-25.578613,-19" +"Constant: {elements: [-0.5], shape: []}" +"Constant: {elements: [0.5], shape: []}" +"Clip: {elements: [-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0." +"Constant: {elements: [0.34180975,0.98490405,0.20374298,0.3445685,0.60249984,0.9405572,7.452351e-2,0.6528334,0." +"Constant: {elements: [0.586107,0.99137485,0.11356729,0.5807301,4.3787777e-2,0.8292314,0.7302829,0.48619032,0.7" +"MatMul: {elements: [-15.098519,-11.611205,-3.7695527,-0.99450964,-9.389294], shape: [5,1]}" +"MatMul: {elements: [-23.364487,-25.763004,-21.128778,-25.39899,-20.12619,-16.109161,-18.095474,-16.709824,-2" +"Constant: {elements: [-1.2], shape: []}" +"Constant: {elements: [1.2], shape: []}" +"Clip: {elements: [-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1." +"MatMul: {elements: [-16.215082], shape: [1,1]}" +"MatMul: {elements: [-2.0688262,-14.0012045,-13.242542,-10.042548,-11.89843,-12.352251,-5.819968,-7.015942,-1" +"Constant: {elements: [3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Identity: {elements: [-3.0], shape: []}" +"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-3.0,-1.8123127,-0.6462102,-0.109026946,-3.0,-2.8452566e-2,-3.0,-3.0" +"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" +"Clip: {elements: [-2.0688262,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3" +"Constant: {elements: [5], shape: [1]}" +"Gather: {elements: [-0.9772779,-0.20515826,1.4693588,-1.420018,-1.1806322,-0.40178093,-0.5788497,1.0544517,-" +"Constant: {elements: [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.6687347,0.73371655,0." +"Constant: {elements: [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.6687347,0.73371655,0." +"MatMul: {elements: [-12.6460285], shape: [1,1]}" +"MatMul: {elements: [-4.2586093,-11.211208,-8.299926,-4.0727377,-1.4304519,-3.8753364,-8.456839,-9.278601,-8." +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-1.4304519,-3.0,-3.0,-3.0,-3.0,-1.7071044,-3.0,-3.0,-3.0,-3.0,-2.425" +"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" +"MatMul: {elements: [-8.988196], shape: [1,1]}" +"MatMul: {elements: [-3.026817,-7.9683943,-5.899193,-2.8947084,-1.0166973,-2.754405,-6.0107193,-6.5947886,-5." +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-3.0,-3.0,-3.0,-2.8947084,-1.0166973,-2.754405,-3.0,-3.0,-3.0,-1.2133287,-3.0,-3.0,-2.83" +"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" +"Constant: {elements: [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62191015,0.26306838,0" +"Constant: {elements: [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62191015,0.26306838,0" +"MatMul: {elements: [-7.23862], shape: [1,1]}" +"MatMul: {elements: [-4.475377,-5.4445786,-3.749067,-0.95438576,-4.7584934,-0.6636875,-4.501771,-1.9042519,-4" +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-3.0,-3.0,-3.0,-0.95438576,-3.0,-0.6636875,-3.0,-1.9042519,-3.0,-0.5509068,-1.5594246,-9" +"MatMul: {elements: [-1.54632], shape: [1,1]}" +"MatMul: {elements: [-0.9560337,-1.1630754,-0.8008788,-0.20387667,-1.0165133,-0.14177747,-0.96167207,-0.40678" +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-0.9560337,-1.1630754,-0.8008788,-0.20387667,-1.0165133,-0.14177747,-0.96167207,-0.40678" +"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" +"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" +"MatMul: {elements: [-12.180203], shape: [1,1]}" +"MatMul: {elements: [-1.5540301,-10.517216,-9.947335,-7.5436115,-8.937685,-9.27858,-4.371757,-5.2701306,-11.8" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-1.5540301,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-2.8304725,-3.0,-3.0,-" +"MatMul: {elements: [-15.17683], shape: [1,1]}" +"MatMul: {elements: [-9.940587,-12.246214,-14.786791,-11.415034,-14.941465,-4.9909825,-1.7796177,-0.3002526,-" +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-1.7796177,-0.3002526,-3.0,-7.835638e-2,-3.0,-3.0,-3.0,-3." +"MatMul: {elements: [-1.072041,-1.8734931,-11.099164,-3.141619,-0.9638957,-3.5323565,-2.4953284,-8.631789,-6." +"Identity: {elements: [-3.0], shape: []}" +"Identity: {elements: [3.0], shape: []}" +"Clip: {elements: [-1.072041,-1.8734931,-3.0,-3.0,-0.9638957,-3.0,-2.4953284,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0," +"MatMul: {elements: [-11.002837], shape: [1,1]}" +"MatMul: {elements: [-3.705257,-9.754454,-7.221455,-3.543537,-1.2445828,-3.3717854,-7.3579793,-8.072964,-7.33" +"Identity: {elements: [-1.0], shape: []}" +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1." +"MatMul: {elements: [12.541454], shape: [1,1]}" +"MatMul: {elements: [7.7539277,9.433143,6.495541,1.6535453,8.244449,1.1498886,7.799658,3.2992601,7.742188,0.9" +"Identity: {elements: [-1.0], shape: []}" +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.95448756,1.0,0.16623473,1.0,1.0,1.0,1.0,1.0,0.4958" +"Identity: {elements: [-1.0], shape: []}" +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,0.5447995,9.1917194e-2,1.0,2.3987466e-2,1.0,1.0,0.9589323,1.0,0." +"Identity: {elements: [1.0], shape: []}" +"Clip: {elements: [-0.31687793,-0.5537741,-1.0,-0.9286116,-0.28491193,-1.0,-0.7375786,-1.0,-1.0,-1.0,-1.0,-" +"Constant: {elements: [6], shape: [1]}" +"Gather: {elements: [0.95008844,0.3130677,0.15494743,-1.7062702,-2.8182229e-2,-1.6301984,-0.31155252,-0.40317" +"Constant: {elements: [7], shape: [1]}" +"Gather: {elements: [-0.1513572,-0.85409576,0.37816253,1.9507754,0.42833188,0.46278226,5.616534e-2,1.222445,0" +"Constant: {elements: [8], shape: [1]}" +"Gather: {elements: [-0.10321885,-2.5529897,-0.88778573,-0.5096522,6.651722e-2,-0.9072984,-1.1651498,0.208274" +"Constant: {elements: [10], shape: [1]}" +"Gather: {elements: [0.14404356,0.8644362,-0.34791216,-1.2527953,-0.6343221,0.7290906,0.46566245,0.3563664,-0" +"Constant: {elements: [11], shape: [1]}" +"Gather: {elements: [1.4542735,-0.742165,0.15634897,0.7774904,-0.36274117,0.12898292,-1.5362437,0.7065732,-0." +"Rowan: concat" +"Concat: {elements: [3.487773,3.487773,3.487773,3.487773,-0.5,-1.2,5.0,0.95507205,0.95507205,0.95507205,0.955" +[500,28] +"Constant: {elements: [-0.18432815,-3.3549756e-2,0.1182383,-8.149214e-2,-0.17400299,3.5006218e-2,6.768849e-3,-5" +"Constant: {elements: [6.1723504e-2], shape: [1,1]}" +"Gemm: {elements: [0.39932898,0.6212738,1.3125417,1.1153207,0.658881,0.89349085,1.0576929,0.9657631,0.98187" +"Output: {elements: [0.39932898,0.6212738,1.3125417,1.1153207,0.658881,0.89349085,1.0576929,0.9657631,0.98187" +"Rowan: Asserting equality" \ No newline at end of file From fd22cf88365acb59eb49ead4181f58894b5c67cf Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 8 Aug 2023 14:50:11 +0100 Subject: [PATCH 66/90] generalised matmul --- src/MatrixForm.hs | 7 ++-- src/Numskull.hs | 80 +++++++++++++++++++++++++++++++++++++------- src/Serialisation.hs | 12 +++---- src/Typing.hs | 3 ++ 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index 98b39f8..151cd8e 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -40,8 +40,8 @@ matrixShape :: TreeMatrix a -> [Integer] matrixShape = treeShape . matrixToTree -- WRITING MATRICIES -printArray :: NdArray -> IO () -printArray (NdArray s v) = putStr $ conc <> "\n" +prettyShowArray :: NdArray -> String +prettyShowArray (NdArray s v) = conc <> "\n" where vl = map show (V.toList v) largest = maximum $ map length vl @@ -50,6 +50,9 @@ printArray (NdArray s v) = putStr $ conc <> "\n" lined = addNewlines newlines spaced conc = concatMap snd lined +printArray :: NdArray -> IO () +printArray nd = putStr $ prettyShowArray nd + padStringTo :: Int -> String -> String padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " diff --git a/src/Numskull.hs b/src/Numskull.hs index 85f515a..3d95d07 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -102,6 +102,7 @@ module Numskull ( -- Pretty printing , printArray + , prettyShowArray -- typing , (=@=) @@ -120,7 +121,7 @@ import Data.Vector.Storable (Vector) import Type.Reflection import qualified Data.Map as M import Data.Maybe (fromJust, isJust) -import Data.List (sort, elemIndex, intersect) +import Data.List (sort, elemIndex, intersect, zipWith4) import Debug.Trace @@ -187,6 +188,14 @@ getVector (NdArray _ v) = v <-@ typeRep @(Vector a) -- | Gets the TypeRep String representation the NdArray elements ndType :: NdArray -> String ndType (NdArray _ v) = show $ vecType v + +checkNdType :: forall a b . (DType a, DType b) => NdArray -> TypeRep a -> Maybe (a :~~: b) +checkNdType (NdArray _ v) t = + let tv = vecType v + in case (eqTypeRep tv (typeRep @b)) of + Just HRefl -> eqTypeRep (typeRep @a) (tv :: TypeRep b) + _ -> error "Impossibly mismatching types." + {- ndType (NdArray _ v) = case v =@= (undefined :: DType a => Vector a) of Just HRefl -> show $ vecType v @@ -201,7 +210,7 @@ extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector extractVectors [] _ = Just [] extractVectors ((NdArray _ v) : nds) t = case v =@= (undefined :: Vector a) of - Just HRefl -> + Just HRefl -> case extractVectors nds t of Just vs -> Just (v:vs) _ -> Nothing @@ -377,10 +386,31 @@ pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then Nothing -> error $ typeMismatch (show$ty v) (show$ty u) else error $ shapeMismatch (show s) (show r) +zipArrayWith :: forall a b c . (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray +zipArrayWith zipfunc (NdArray s v) (NdArray r u) = + let + -- Truncate the shapes to match each other + ndC1 = constrainShape r (NdArray s v) + ndC2 = constrainShape s (NdArray r u) + s' = shape ndC1 + in + -- Type check the function + case (v =@ typeRep @(Vector a), u =@ typeRep @(Vector b)) of + (Just HRefl, Just HRefl) -> + let + v' = getVector ndC1 :: Vector a + u' = getVector ndC2 :: Vector b + in NdArray s' (V.zipWith zipfunc v' u' :: Vector c) + _ -> error "Type mismatch" + -- Todo: Needs to operate on doubles --elemDivide :: NdArray -> NdArray -> NdArray --elemDivide = pointwiseZip divide +-- | Pointwise integer division +--elemDiv :: NdArray -> NdArray -> NdArray +--elemDiv = pointwiseZip DType.div + -- | Pointwise division elemDivide :: NdArray -> NdArray -> NdArray elemDivide = pointwiseZip DType.divide @@ -486,10 +516,18 @@ padShape r (NdArray s v) = then NdArray r (V.unsafeUpdate_ nullVec newIndices v) else error "Cannot map to a smaller shape." +constrainShape :: [Integer] -> NdArray -> NdArray +constrainShape r (NdArray s v) = + let + s' = zipWith min r s + sPad = s' ++ replicate (length s - length r) 1 + in NdArray s' $ + V.ifilter (\i _ -> and $ zipWith (<) (expandInd s (toInteger i)) sPad) v + --broadcast :: forall a . DType a => (NdArray, NdArray) -> Maybe (Vector a, Vector a) broadcast :: (NdArray, NdArray) -> Maybe (NdArray, NdArray) -broadcast ((NdArray s v), (NdArray r u)) = - let +broadcast ((NdArray s v), (NdArray r u)) = + let (s',v',r',u') = broadcastDimensions s v r u newshape = sequenceA $ zipWith (\x y -> if x == y || x == 1 || y == 1 then Just (max x y) else Nothing) s' r' @@ -505,7 +543,7 @@ broadcast ((NdArray s v), (NdArray r u)) = broadcastDimensions :: (DType a, DType b) => [Integer] -> Vector a -> [Integer] -> Vector b -> ([Integer], Vector a, [Integer], Vector b) -broadcastDimensions s v r u +broadcastDimensions s v r u | sl == rl = (s,v, r,u) | sl > rl = (s,v, @@ -752,16 +790,34 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm dot :: DType a => NdArray -> NdArray -> a dot nd1 nd2 = foldrA (DType.add) (DType.addId) (nd1*nd2) --- For now, just nxm and mxp = nxp +-- matMul :: NdArray -> NdArray -> NdArray matMul (NdArray s v) (NdArray r u) = - if (length s /= 2) || (length r /= 2) || s!!1 /= r!!0 then - error "Matricies must be 2D and the number of columns in the first must match the number of rows in the second." - else case v =@= u of - Just HRefl -> NdArray sh (matMulVec s v r u) + case v =@= u of + Just HRefl -> + case (reverse s, reverse r) of + -- Standard matrix multiplication + ([m, n], [q, p]) | m == p -> NdArray [n,q] (matMulVec s v r u) + -- 1D arrays have the extra dimension pre/appended then result collapses back to 1D + ([m], [q, p]) | m == p -> NdArray [q] (matMulVec [1,m] v r u) + ([m, n], [p]) | m == p -> NdArray [n] (matMulVec s v [p,1] u) + -- ND-arrays are broadcast to match each other where possible and treated as + -- stacks of nxm/pxq arrays. + ((m : n : ss), (q : p : rs)) | m == p -> + let + (s', v', r', u') = broadcastDimensions s v r u + stackA = vectorChunksOf (fromIntegral @Integer @Int $ m * n) v' + stackB = vectorChunksOf (fromIntegral @Integer @Int $ q * p) u' + stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,q]) stackB + in + NdArray (take (length s' -2) s' ++ [n,q]) $ V.concat stackAB + _ -> error "Invalid matrix dimensions for multiplication" _ -> error "Cannot multiply matricies of two distinct types." - where - sh = [s!!0, r!!1] + +vectorChunksOf :: V.Storable a => Int -> Vector a -> [Vector a] +vectorChunksOf _ v | V.null v = [] +vectorChunksOf n v = first : (vectorChunksOf n rest) + where (first, rest) = V.splitAt n v -- returning the vector result of the matMul matMulVec :: forall a . DType a => diff --git a/src/Serialisation.hs b/src/Serialisation.hs index d14e68a..a67096b 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -9,7 +9,7 @@ module Serialisation where import DType import NdArray -import Data.Int() +import Data.Int import System.IO import Data.List as List import Data.List.Split @@ -130,12 +130,12 @@ loadPayload h sh _ = do reifyDType :: String -> (forall a . DType a => TypeRep a -> r) -> r reifyDType dtype cont = case dtype of - -- " cont (typeRep @Int64) - " cont (typeRep @Int) - -- " cont (typeRep @Int32) + " cont (typeRep @Int64) + --" cont (typeRep @Int) + " cont (typeRep @Int32) " cont (typeRep @Float) - -- " cont (typeRep @Double) - -- " cont (typeRep @Char) + " cont (typeRep @Double) + " cont (typeRep @Char) " cont (typeRep @Bool) _ -> error "Unsupported dtype." diff --git a/src/Typing.hs b/src/Typing.hs index 5153f12..66df69b 100644 --- a/src/Typing.hs +++ b/src/Typing.hs @@ -18,6 +18,9 @@ ty = typeOf (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) +(=@) :: Typeable a => a -> TypeRep b -> Maybe (a :~~: b) +(=@) x t = eqTypeRep (ty x) t + -- Helper asserting a type (<-@) ::Typeable a => a -> TypeRep b -> b (<-@) val t = case eqTypeRep t (ty val) of From a6f2bd0e3e68c8897aa04db3ca13416e1ab53170 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 8 Aug 2023 17:36:00 +0100 Subject: [PATCH 67/90] summation --- src/Numskull.hs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index 3d95d07..067f28a 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -423,6 +423,32 @@ elemDivide = pointwiseZip DType.divide elemPow :: NdArray -> NdArray -> NdArray elemPow = pointwiseZip DType.pow +----- Many Arguments + +-- unsafe over different types +sum :: [NdArray] -> NdArray +sum ((NdArray s v) : nds) = foldr (\x acc -> (padShape sh x) + acc) (zeros (vecType v) sh) ((NdArray s v) : nds) + where sh = maximiseShape (map shape nds) + +maximiseShape :: [[Integer]] -> [Integer] +maximiseShape [] = [] +maximiseShape [sh] = sh +maximiseShape (sh : shs) = + let + m = maximiseShape shs + diff = length sh - length m + in + if diff > 0 + then zipWith max sh ((take diff sh) ++ m) + else zipWith max (take (-diff) m ++ sh) m + + +--mean :: [NdArray] -> NdArray- +--mean [] = NdArray [] $ V.fromList [] +--mean [nd] = nd + + + -- * Type & Shape Conversion {- | Converting between the standard dtypes and changing the shapes of arrays. NB the difference between 'size' and 'shape'. The shape is an Integer list @@ -649,7 +675,7 @@ concatAlongVec vs shs axis = else let axDim = axisDimensions axis shs - newshape = replaceNth axis (sum axDim) (head shs) + newshape = replaceNth axis (Prelude.sum axDim) (head shs) -- first is sub-array number, second is sub-array index arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim (newMultiInds, _) = mapIndicies newshape From 382f1d381b2d2558ca7114e19d70c0ac3f3ab4d8 Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 9 Aug 2023 17:31:16 +0100 Subject: [PATCH 68/90] much tidying --- .hlint.yaml | 30 +++ .stylish-haskell.yaml | 171 +++++++++++++++ src/DType.hs | 24 +- src/NdArrayException.hs | 45 ++++ src/Numskull.hs | 462 +++++++++++++++++++-------------------- test/Test/aikens_out.txt | 195 ----------------- test/Test/rowans_out.txt | 194 ---------------- 7 files changed, 480 insertions(+), 641 deletions(-) create mode 100644 .hlint.yaml create mode 100644 .stylish-haskell.yaml create mode 100644 src/NdArrayException.hs delete mode 100644 test/Test/aikens_out.txt delete mode 100644 test/Test/rowans_out.txt diff --git a/.hlint.yaml b/.hlint.yaml new file mode 100644 index 0000000..3f1b271 --- /dev/null +++ b/.hlint.yaml @@ -0,0 +1,30 @@ +##################################################################### +## HINTS + +- functions: + - {name: Test.Hspec.focus, within: []} # focus should only be used for debugging + - {name: Prelude.undefined, within: []} # Prelude.undefined should only be used temporarily + - {name: Clash.XException.undefined, within: []} # Clash undefined should only be used temporarily (use deepErrorX instead) + +- error: {lhs: fromString (show x), rhs: Data.String.Extra.show' x} +- error: {lhs: Data.Text.pack (show x), rhs: Data.String.Extra.show' x} +- error: {lhs: fromIntegral (Clash.Promoted.Nat.snatToInteger x), rhs: Clash.Promoted.Nat.snatToNum x} +- error: {lhs: Clash.Sized.Internal.BitVector.split# (ClaSH.Class.BitPack.pack x), rhs: Clash.Prelude.BitIndex.split} +- error: {lhs: Clash.Signal.mux p (fmap Just x) (pure Nothing), rhs: Clash.Signal.Extra.boolToMaybe p x} +- error: {lhs: Clash.Signal.mux p (Just <$> x) (pure Nothing), rhs: Clash.Signal.Extra.boolToMaybe p x} +- error: {lhs: Clash.Prelude.moore x id, rhs: Clash.Prelude.Moore.medvedev x} +- error: {lhs: Clash.Prelude.medvedev f x (pure ()), rhs: Clash.Source.source' (flip f ()) x} + +# We tend to use pure over return +- error: {lhs: return, rhs: pure} +- error: {lhs: ceiling (logBase 2 (fromIntegral x)), rhs: Numeric.Natural.Extra.fromNatural (Numeric.Log2.clog2 (fromIntegral x))} +- error: {lhs: floor (logBase 2 (fromIntegral x)), rhs: Numeric.Natural.Extra.fromNatural (Numeric.Log2.flog2 (fromIntegral x))} +- error: {lhs: ceiling (logBase 4 (fromIntegral x)), rhs: Numeric.Natural.Extra.fromNatural (Numeric.Log2.clog4 (fromIntegral x))} +- error: {lhs: floor (logBase 4 (fromIntegral x)), rhs: Numeric.Natural.Extra.fromNatural (Numeric.Log2.flog4 (fromIntegral x))} + +# We all know when it's appropriate to use [Char] +- ignore: {name: Use String} +- ignore: {name: Use head} +- ignore: {name: Reduce duplication} +- ignore: {name: Use tuple-section} +- ignore: {name: Use <$>} diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml new file mode 100644 index 0000000..539a00d --- /dev/null +++ b/.stylish-haskell.yaml @@ -0,0 +1,171 @@ +# stylish-haskell configuration file +# ================================== + +# The stylish-haskell tool is mainly configured by specifying steps. These steps +# are a list, so they have an order, and one specific step may appear more than +# once (if needed). Each file is processed by these steps in the given order. +steps: + # Convert some ASCII sequences to their Unicode equivalents. This is disabled + # by default. + # - unicode_syntax: + # # In order to make this work, we also need to insert the UnicodeSyntax + # # language pragma. If this flag is set to true, we insert it when it's + # # not already present. You may want to disable it if you configure + # # language extensions using some other method than pragmas. Default: + # # true. + # add_language_pragma: true + + # Align the right hand side of some elements. This is quite conservative + # and only applies to statements where each element occupies a single + # line. + - simple_align: + cases: false + top_level_patterns: false + records: true + + # Import cleanup + - imports: + # There are different ways we can align names and lists. + # + # - global: Align the import names and import list throughout the entire + # file. + # + # - file: Like global, but don't add padding when there are no qualified + # imports in the file. + # + # - group: Only align the imports per group (a group is formed by adjacent + # import lines). + # + # - none: Do not perform any alignment. + # + # Default: global. + align: group + + # Folowing options affect only import list alignment. + # + # List align has following options: + # + # - after_alias: Import list is aligned with end of import including + # 'as' and 'hiding' keywords. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_alias: Import list is aligned with start of alias or hiding. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - new_line: Import list starts always on new line. + # + # > import qualified Data.List as List + # > (concat, foldl, foldr, head, init, last, length) + # + # Default: after_alias + list_align: after_alias + + # Long list align style takes effect when import is too long. This is + # determined by 'columns' setting. + # + # - inline: This option will put as much specs on same line as possible. + # + # - new_line: Import list will start on new line. + # + # - new_line_multiline: Import list will start on new line when it's + # short enough to fit to single line. Otherwise it'll be multiline. + # + # - multiline: One line per import list entry. + # Type with contructor list acts like single import. + # + # > import qualified Data.Map as M + # > ( empty + # > , singleton + # > , ... + # > , delete + # > ) + # + # Default: inline + long_list_align: new_line + + # List padding determines indentation of import list on lines after import. + # This option affects 'list_align' and 'long_list_align'. + list_padding: 2 + + # Separate lists option affects formating of import list for type + # or class. The only difference is single space between type and list + # of constructors, selectors and class functions. + # + # - true: There is single space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable (fold, foldl, foldMap)) + # + # - false: There is no space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable(fold, foldl, foldMap)) + # + # Default: true + separate_lists: true + + # Language pragmas + - language_pragmas: + # We can generate different styles of language pragma lists. + # + # - vertical: Vertical-spaced language pragmas, one per line. + # + # - compact: A more compact style. + # + # - compact_line: Similar to compact, but wrap each line with + # `{-#LANGUAGE #-}'. + # + # Default: vertical. + style: vertical + + # Align affects alignment of closing pragma brackets. + # + # - true: Brackets are aligned in same collumn. + # + # - false: Brackets are not aligned together. There is only one space + # between actual import and closing bracket. + # + # Default: true + align: false + + # stylish-haskell can detect redundancy of some language pragmas. If this + # is set to true, it will remove those redundant pragmas. Default: true. + remove_redundant: true + + # Replace tabs by spaces. This is disabled by default. + # - tabs: + # # Number of spaces to use for each tab. Default: 8, as specified by the + # # Haskell report. + # spaces: 8 + + # Remove trailing whitespace + - trailing_whitespace: {} + +# A common setting is the number of columns (parts of) code will be wrapped +# to. Different steps take this into account. Default: 80. +columns: 100 + +# By default, line endings are converted according to the OS. You can override +# preferred format here. +# +# - native: Native newline format. CRLF on Windows, LF on other OSes. +# +# - lf: Convert to LF ("\n"). +# +# - crlf: Convert to CRLF ("\r\n"). +# +# Default: native. +newline: lf + +# Sometimes, language extensions are specified in a cabal file or from the +# command line instead of using language pragmas in the file. stylish-haskell +# needs to be aware of these, so it can parse the file correctly. +# +# No language extensions are enabled by default. +language_extensions: + - MultiParamTypeClasses + - FlexibleContexts diff --git a/src/DType.hs b/src/DType.hs index ff0f0b1..16ad06f 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -18,11 +18,11 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where subtract :: a -> a -> a multiply :: a -> a -> a divide :: a -> a -> a - div :: a -> a -> Integer + div :: a -> a -> Int power :: a -> Double -> Double pow :: a -> a -> a log :: a -> a -> a - mod :: a -> a -> Integer + mod :: a -> a -> Int abs :: a -> a signum :: a -> a ceil :: a -> a @@ -47,13 +47,13 @@ instance DType Int where subtract x y = x - y multiply x y = x * y divide x y = P.div x y - div x y = (fromIntegral $ P.div x y) :: Integer + div x y = (fromIntegral $ P.div x y) :: Int power x d = fromIntegral x ** d pow x y = x ^ y log x y = (P.floor $ logBase xd yd) :: Int where xd = fromIntegral @Int @Double x yd = fromIntegral @Int @Double y - mod x y = fromIntegral (x `P.mod` y) :: Integer + mod = P.mod abs = P.abs signum = P.signum ceil x = x @@ -82,13 +82,13 @@ instance DType Int32 where subtract x y = x - y multiply x y = x * y divide x y = P.div x y - div x y = (fromIntegral $ P.div x y) :: Integer + div x y = fromIntegral @Int32 @Int $ P.div x y power x d = fromIntegral x ** d pow x y = x ^ y log x y = (P.floor $ logBase xd yd) :: Int32 where xd = fromIntegral @Int32 @Double x yd = fromIntegral @Int32 @Double y - mod x y = fromIntegral (x `P.mod` y) :: Integer + mod x y = fromIntegral @Int32 @Int $ P.mod x y abs = P.abs signum = P.signum ceil x = x @@ -113,13 +113,13 @@ instance DType Int64 where subtract x y = x - y multiply x y = x * y divide x y = P.div x y - div x y = (fromIntegral $ P.div x y) :: Integer + div x y = fromIntegral @Int64 @Int $ P.div x y power x d = fromIntegral x ** d pow x y = x ^ y log x y = (P.floor $ logBase xd yd) :: Int64 where xd = fromIntegral @Int64 @Double x yd = fromIntegral @Int64 @Double y - mod x y = fromIntegral (x `P.mod` y) :: Integer + mod x y = fromIntegral @Int64 @Int $ P.mod x y abs = P.abs signum = P.signum ceil x = x @@ -148,8 +148,7 @@ instance DType Float where power x d = float2Double x ** d pow x y = x ** y log x y = logBase x y - mod x y = fromIntegral @Int @Integer (xi `P.mod` yi) - where xi = P.floor x; yi = P.floor y + mod x y = P.floor x `P.mod` P.floor y abs = P.abs signum = P.signum ceil = fromIntegral @Integer @Float . P.ceiling @@ -178,8 +177,7 @@ instance DType Double where power x d = x ** d pow x y = x ** y log x y = logBase x y - mod x y = fromIntegral @Int @Integer (xi `P.mod` yi) :: Integer - where xi = P.floor x; yi = P.floor y + mod x y = P.floor x `P.mod` P.floor y abs = P.abs signum = P.signum ceil = fromIntegral @Integer @Double . P.ceiling @@ -208,7 +206,7 @@ instance DType Bool where power _x _d = undefined pow x y = toEnum (fromEnum x ^ fromEnum y) log _x _y = undefined - mod x y = fromIntegral (fromEnum x `P.mod` fromEnum y) :: Integer + mod x y = fromEnum x `P.mod` fromEnum y abs _ = True signum = id ceil = id diff --git a/src/NdArrayException.hs b/src/NdArrayException.hs new file mode 100644 index 0000000..2a15314 --- /dev/null +++ b/src/NdArrayException.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module NdArrayException where + +import Control.Exception +import Type.Reflection +import Data.Vector.Storable (Vector) + +import DType +import NdArray + +data NdArrayException + = DTypeMismatch NdArray NdArray String + | ShapeMismatch NdArray NdArray String + | CreationSize Integer [Integer] + | TypeMismatch String + +instance Exception NdArrayException + +instance Show NdArrayException where + show (DTypeMismatch (NdArray _ v) (NdArray _ u) extra) = + if extra == "" then + "Cannot match NdArrays of type '" <> showType v <> + "' and type '" <> showType u <> "'." + else + "Cannot perform " <> extra <> " on mismatching NdArrays of type '" <> showType v <> + "' and type '" <> showType u <> "'." + + show (ShapeMismatch (NdArray s _) (NdArray r _) extra) = + if extra == "" then + "Cannot match NdArrays of shape " <> show s <> + " and shape " <> show r <> "." + else + "Cannot perform " <> extra <> " on mismatching NdArrays of shape " <> show s <> + " and shape " <> show r <> "." + + show (CreationSize sz sh) = + "Cannot create array of size " <> show sz <> " and shape " <> show sh <> "." + + show (TypeMismatch str) = str + +showType :: forall a . DType a => Vector a -> String +showType _ = show (typeRep @a) diff --git a/src/Numskull.hs b/src/Numskull.hs index 067f28a..6e4db31 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -12,10 +12,11 @@ module Numskull ( , shape , getVector , ndType + , checkNdType + , isEmpty -- Creation , NdArray - , NdArray.NdArray , fromList , fromListFlat , TreeMatrix @@ -24,6 +25,7 @@ module Numskull ( , singleton , arange , zeros + , squareArr -- General mapping, folding & zipping , foldrA @@ -31,6 +33,7 @@ module Numskull ( , mapTransform , pointwiseZip , pointwiseBool + , zipArrayWith -- Summaries , origin @@ -52,7 +55,11 @@ module Numskull ( -- Mathematical pointwise , elemDivide - , elemPow + , elemDiv + , elemPow + , elemPower + , Numskull.sum + , mean -- Bounds , clip @@ -62,14 +69,12 @@ module Numskull ( , matchDType -- Size conversions - , constrainSize - , padSize - , setSize , resize -- Shape conversions/manipulations , reshape , padShape + , constrainShape , broadcast , concatAlong , gather @@ -115,12 +120,14 @@ import DType (DType) import MatrixForm import Indexing import Typing +import NdArrayException +import Control.Exception import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import Type.Reflection import qualified Data.Map as M -import Data.Maybe (fromJust, isJust) +import Data.Maybe (fromJust) import Data.List (sort, elemIndex, intersect, zipWith4) import Debug.Trace @@ -129,13 +136,16 @@ import Debug.Trace -- >>> import Numskull as N -- >>> import qualified Vector +-- * Numeric & Comparative NdArray instances: +-------------------------------------------------------------------------------- + instance Eq NdArray where -- | Arrays are equal if their elements and shape exactly match. - (NdArray s v) == (NdArray r u) = (r == s) && + (NdArray s v) == (NdArray r u) = (r == s) && case v =@= u of Just HRefl -> v == u Nothing -> False - (NdArray s v) /= (NdArray r u) = (r /= s) || + (NdArray s v) /= (NdArray r u) = (r /= s) || case v =@= u of Just HRefl -> v /= u Nothing -> True @@ -146,15 +156,14 @@ instance Ord NdArray where -} (NdArray s v) `compare` (NdArray r u) = if s == r then case v =@= u of Just HRefl -> compare v u - Nothing -> error $ typeMismatch (show v) (show u) - else error $ shapeMismatch (show s) (show r) + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "compare") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "compare") (NdArray s v) <= (NdArray r u) = if s == r then case v =@= u of Just HRefl -> v <= u - Nothing -> error $ typeMismatch (show v) (show u) - else error $ shapeMismatch (show s) (show r) + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "'<='") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "'<='") --- To do: change fromInteger to return an integer array rather than int instance Num NdArray where -- | Adds elements pointwise (+) = pointwiseZip DType.add @@ -168,10 +177,11 @@ instance Num NdArray where abs (NdArray s v) = NdArray s (V.map DType.abs v) -- | Signum of each element signum (NdArray s v) = NdArray s (V.map DType.signum v) - -- Creates a singleton array + -- Creates a singleton array. NB: must be converted to a storable Int. fromInteger = singleton . fromInteger @Int --- * Creation & Miscellaneous +-- * General & Creation +-------------------------------------------------------------------------------- -- | Gets the total number of elements in a given array shape. -- >>> size [2,3] @@ -179,32 +189,34 @@ instance Num NdArray where size :: [Integer] -> Int size sh = (fromIntegral $ product sh) :: Int +-- | Returns the shape list of an array. shape :: NdArray -> [Integer] shape (NdArray s _) = s +-- | Gets the vector of an array. Requires a type specification to output safely. getVector :: forall a . DType a => NdArray -> Vector a getVector (NdArray _ v) = v <-@ typeRep @(Vector a) --- | Gets the TypeRep String representation the NdArray elements +-- | Gets the TypeRep String representation of the NdArray elements ndType :: NdArray -> String ndType (NdArray _ v) = show $ vecType v +-- | Compares the type of the array elements to the given TypeRep. checkNdType :: forall a b . (DType a, DType b) => NdArray -> TypeRep a -> Maybe (a :~~: b) -checkNdType (NdArray _ v) t = +checkNdType (NdArray _ v) _ = let tv = vecType v - in case (eqTypeRep tv (typeRep @b)) of + in case eqTypeRep tv (typeRep @b) of Just HRefl -> eqTypeRep (typeRep @a) (tv :: TypeRep b) _ -> error "Impossibly mismatching types." -{- -ndType (NdArray _ v) = case v =@= (undefined :: DType a => Vector a) of - Just HRefl -> show $ vecType v - _ -> error "Impossible type mismatch." --} --- Helper to get the vector typeRep +-- | Helper to get the vector typeRep. vecType :: forall a . DType a => Vector a -> TypeRep a vecType _ = typeRep @a +-- | Checks if the undelying vector has any elements. +isEmpty :: NdArray -> Bool +isEmpty (NdArray _ v) = V.null v + -- | Convert a list of arrays to a list of vectors, provided they are all of the specified type. extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] extractVectors [] _ = Just [] @@ -216,13 +228,9 @@ extractVectors ((NdArray _ v) : nds) t = _ -> Nothing Nothing -> Nothing --- Todo: get the ident of the dtype from an nd array -identityElem :: forall a . DType a => NdArray -> a -identityElem (NdArray s v) = indentityElem' v <-@ typeRep @a - --- Helper for the vectors in identityElem -indentityElem' :: forall a . DType a => Vector a -> a -indentityElem' _ = DType.addId :: DType a => a +-- Gets the DType additive identity matching the element type of a vector. +identityElem :: forall a . DType a => Vector a -> a +identityElem _ = DType.addId :: DType a => a -- | Creates an NdArray from a given shape and list. The number of elements must match. -- >>> printArray $ fromList [2,2] [1,2,3,4::Int] @@ -230,7 +238,7 @@ indentityElem' _ = DType.addId :: DType a => a -- 3 4 fromList :: DType a => [Integer] -> [a] -> NdArray fromList sh l = - if length l /= size sh then error "Length of the list should match the total number of elements specified by the shape." + if length l /= size sh then throw $ CreationSize (fromIntegral $ length l) sh else NdArray sh (V.fromList l) -- | Creates a 1xn NdArray from a list. @@ -267,24 +275,34 @@ singleton x = NdArray [1] (V.fromList [x]) -- | Creates a flat array over the specified range. arange :: (Enum a, DType a) => a -> a -> NdArray -arange mini maxi = if mini <= maxi then NdArray [fromIntegral $ fromEnum maxi - fromEnum mini] $ V.fromList [mini..maxi] - else error "Minimum of range is higher than maximum." +arange mini maxi = + if mini <= maxi + then NdArray [fromIntegral $ fromEnum maxi - fromEnum mini + 1] $ V.fromList [mini..maxi] + else NdArray [] (V.fromList [] :: Vector Int) {- | Creates the smallest possible square matrix from the given list, padding out any required space with the identity element for the DType -} -squareArr = undefined +squareArr :: forall a . DType a => [a] -> NdArray +squareArr [] = NdArray [] (V.fromList [] :: Vector Int) +squareArr xs = + let + l = length xs + d = ceiling (sqrt $ fromIntegral @Int @Float l) + d' = fromIntegral @Int @Integer d + p = V.replicate (d^(2::Int) - l) (DType.addId :: a) + in NdArray [d', d'] (V.fromList xs V.++ p) {- | Creates an array of the given shape of the identity element for the given type. -} zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray zeros _ s = NdArray s zerovec where - ident = (DType.addId :: DType a => a) + ident = DType.addId :: (DType a => a) zerovec = (V.replicate (size s) ident) :: DType a => Vector a --- * Pointwise Functions -- --- All the numpy-like functions not defined within the Eq, Ord or Num instances +-- * Pointwise Functions +-------------------------------------------------------------------------------- ------ One Argument +-- * One Argument {- | Near identical to a standard foldr instance, expect NdArrays do not have an explicit type. Folds in row-major order. @@ -293,80 +311,93 @@ foldrA :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b foldrA f z (NdArray _ v) = case v =@= (undefined :: Vector a) of Just HRefl -> V.foldr f z v - _ -> error "Starting value type does not match array type." + _ -> throw $ TypeMismatch "Fold starting value type does not match array type." -- | Near identical to a standard map implementation in row-major order. mapA :: forall a . forall b . (DType a, DType b) => (a -> b) -> NdArray -> NdArray mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of Just HRefl -> NdArray s (V.map f v) - _ -> error "Function input does not match array type." + _ -> throw $ TypeMismatch "Map function input does not match array type." -- | Maps functions which return the same type. mapTransform :: (forall a . DType a => a -> a) -> NdArray -> NdArray mapTransform f (NdArray s v) = NdArray s (V.map f v) +-- | Multiplies all elements by a scalar. scale :: forall a . DType a => a -> NdArray -> NdArray -scale x = mapA (DType.multiply x) +scale x = mapA DType.multiply x +-- | Takes the absolute value of all elements. absA :: NdArray -> NdArray -absA = mapTransform (DType.abs) +absA = mapTransform DType.abs +-- | Replaces all elements by their signum. +-- >>> printArray $ signumA (fromList [5] [-50, -25, 0, 1, 10::Int]) +-- -1 -1 0 1 1 signumA :: NdArray -> NdArray -signumA = mapTransform (DType.signum) +signumA = mapTransform DType.signum +-- | Mathematical ceiling of each element (preserving DType). ceilA :: NdArray -> NdArray -ceilA = mapTransform (DType.ceil) +ceilA = mapTransform DType.ceil +-- | Mathematical floor of each element (preserving DType). floorA :: NdArray -> NdArray -floorA = mapTransform (DType.floor) +floorA = mapTransform DType.floor +-- | Sine of each element (preserving DType). sinA :: NdArray -> NdArray -sinA = mapTransform (DType.sin) +sinA = mapTransform DType.sin +-- | Cosine of each element (preserving DType). cosA :: NdArray -> NdArray -cosA = mapTransform (DType.cos) +cosA = mapTransform DType.cos +-- | Tangent of each element (preserving DType). tanA :: NdArray -> NdArray -tanA = mapTransform (DType.tan) +tanA = mapTransform DType.tan +-- | Either elementwise NOT or NEG depending on the DType. invertA :: NdArray -> NdArray -invertA = mapTransform (DType.invert) +invertA = mapTransform DType.invert +-- | Multiply each element by 2. shiftleftA :: NdArray -> NdArray -shiftleftA = mapTransform (DType.shiftleft) +shiftleftA = mapTransform DType.shiftleft +-- | Divide each element by 2. shiftrightA :: NdArray -> NdArray -shiftrightA = mapTransform (DType.shiftright) +shiftrightA = mapTransform DType.shiftright +-- | Returns the element at the 0th position of the array. origin :: forall a . DType a => NdArray -> a -origin (NdArray s v) = (v V.! 0) <-@ typeRep @a +origin (NdArray _ v) = (v V.! 0) <-@ typeRep @a +-- | Returns the largest element. maxElem :: forall a . DType a => NdArray -> a maxElem nd = foldrA max (origin nd) nd +-- | Returns the smallest element. minElem :: forall a . DType a => NdArray -> a minElem nd = foldrA min (origin nd) nd -bound :: Ord a => a -> a -> a -> a -bound mini maxi x - | x <= mini = mini - | x >= maxi = maxi - | otherwise = x - --- NB must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd +-- | Constrains all elements of the array to the range specified by [mini, maxi]. +-- If they are given as Nothing, the range is infinite in that direction. +-- NB: must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd clip :: forall a . DType a => Maybe a -> Maybe a -> NdArray -> NdArray clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of Just HRefl -> case (mini, maxi) of - (Just mn, Just mx) -> mapA (\x -> bound mn mx x) (NdArray s v) - (Just mn, Nothing) -> mapA (\x -> bound mn x x) (NdArray s v) - (Nothing, Just mx) -> mapA (\x -> bound x mx x) (NdArray s v) + (Just mn, Just mx) -> mapA (\x -> if x <= mn then mn else if x >= mx then mx else x) (NdArray s v) + (Just mn, Nothing) -> mapA (\x -> if x <= mn then mn else x) (NdArray s v) + (Nothing, Just mx) -> mapA (\x -> if x >= mx then mx else x) (NdArray s v) (Nothing, Nothing) -> (NdArray s v) - _ -> error "Min and max types do not match array type." + _ -> throw (TypeMismatch $ "Min and max types do not match array type of " <> show (vecType v) <> ".") ------ Two Arguments +-- * Two Arguments --- | The generic function for operating on two DType arrays with the same shape in an element-wise/pointwise way. +-- | The generic function for operating on two matching DType arrays with the same shape +-- in an element-wise/pointwise way. Errors if mismatching -- >>> x = fromList [2,2] [1,2,3,4 :: Int] -- >>> y = fromList [2,2] [5,2,2,2 :: Int] -- >>> printArray $ pointwiseZip (DType.multiply) x y @@ -375,20 +406,23 @@ clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> NdArray s (V.zipWith zipfunc v u) -- Types match - Nothing -> error $ typeMismatch (show$ty v) (show$ty u) - else error $ shapeMismatch (show s) (show r) + Just HRefl -> NdArray s (V.zipWith zipfunc v u) + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") +-- | A slightly specialised version of pointwise zip intended for comparative functions. pointwiseBool :: (forall t . DType t => t -> t -> Bool) -> NdArray -> NdArray -> NdArray pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> NdArray s (V.zipWith zipfunc v u) -- Types match - Nothing -> error $ typeMismatch (show$ty v) (show$ty u) - else error $ shapeMismatch (show s) (show r) + Just HRefl -> NdArray s (V.zipWith zipfunc v u) + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") +-- | Completely generic zip on two NdArrays. If the shapes mismatch, they are truncated as with +-- standard zips. Function inputs must match the DTypes. zipArrayWith :: forall a b c . (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray zipArrayWith zipfunc (NdArray s v) (NdArray r u) = - let + let -- Truncate the shapes to match each other ndC1 = constrainShape r (NdArray s v) ndC2 = constrainShape s (NdArray r u) @@ -401,55 +435,75 @@ zipArrayWith zipfunc (NdArray s v) (NdArray r u) = v' = getVector ndC1 :: Vector a u' = getVector ndC2 :: Vector b in NdArray s' (V.zipWith zipfunc v' u' :: Vector c) - _ -> error "Type mismatch" + _ -> throw (TypeMismatch "Cannot zip NdArrays with different dtypes to the zip function.") --- Todo: Needs to operate on doubles ---elemDivide :: NdArray -> NdArray -> NdArray ---elemDivide = pointwiseZip divide +-- | Pointwise integer division. Will return an NdArray of type Int. +elemDiv :: NdArray -> NdArray -> NdArray +elemDiv (NdArray s v) (NdArray r u) = if s == r then + case v =@= u of + Just HRefl -> elemDivVec s v r u + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemDiv") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "elemDiv") --- | Pointwise integer division ---elemDiv :: NdArray -> NdArray -> NdArray ---elemDiv = pointwiseZip DType.div +elemDivVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector a -> NdArray +elemDivVec s v r u = zipArrayWith (DType.div :: a -> a -> Int) (NdArray s v) (NdArray r u) -- | Pointwise division elemDivide :: NdArray -> NdArray -> NdArray elemDivide = pointwiseZip DType.divide --- Todo: Needs to operate on doubles ---elemPower :: NdArray -> NdArray -> NdArray ---elemPower = pointwiseZip power - --- | Pointwise exponentiation +-- | Pointwise exponentiation (preserving DType) elemPow :: NdArray -> NdArray -> NdArray elemPow = pointwiseZip DType.pow ------ Many Arguments +-- | Pointwise exponentiation which forces precision. +-- Takes some NdArray of bases, an array of Double exponents and returns an array of Doubles. +elemPower :: NdArray -> NdArray -> NdArray +elemPower (NdArray s v) (NdArray r u) = if s == r then + case u =@ typeRep @(Vector Double) of + Just HRefl -> elemPowerVec s v r u + Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemPower") + else throw (ShapeMismatch (NdArray s v) (NdArray r u) "elemPower") + +elemPowerVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector Double -> NdArray +elemPowerVec s v r u = zipArrayWith (DType.power :: a -> Double -> Double) (NdArray s v) (NdArray r u) --- unsafe over different types +-- * Many Arguments + +-- | Takes the pointwise sum over all the given NdArrays. If they are different shapes, +-- the smaller dimensions are padded out with the identity element. +-- The sum of the empty list is the singleton 0. sum :: [NdArray] -> NdArray -sum ((NdArray s v) : nds) = foldr (\x acc -> (padShape sh x) + acc) (zeros (vecType v) sh) ((NdArray s v) : nds) +sum [] = singleton (0::Int) +sum [nd] = nd +sum ((NdArray s v) : nds) = foldr (\x acc -> padShape sh x + acc) (zeros (vecType v) sh) ((NdArray s v) : nds) where sh = maximiseShape (map shape nds) +-- Takes the maximum of each element pointwise matching from the end. maximiseShape :: [[Integer]] -> [Integer] maximiseShape [] = [] maximiseShape [sh] = sh maximiseShape (sh : shs) = - let + let m = maximiseShape shs diff = length sh - length m in if diff > 0 - then zipWith max sh ((take diff sh) ++ m) + then zipWith max sh (take diff sh ++ m) else zipWith max (take (-diff) m ++ sh) m - ---mean :: [NdArray] -> NdArray- ---mean [] = NdArray [] $ V.fromList [] ---mean [nd] = nd - - +-- | Finds the mean pointwise over the list of arrays. Smaller arrays are padded out with +-- the identity element. +mean :: [NdArray] -> NdArray +mean [] = NdArray [] $ V.fromList ([] :: [Int]) +mean nds = s `elemDivide` NdArray sh (V.replicate (size sh) (length nds)) + where + s = Numskull.sum nds + sh = shape s -- * Type & Shape Conversion +-------------------------------------------------------------------------------- + {- | Converting between the standard dtypes and changing the shapes of arrays. NB the difference between 'size' and 'shape'. The shape is an Integer list describing the width of each dimension. Size refers to the total number of @@ -472,7 +526,7 @@ convertDTFromTo _ _ (NdArray s v) = case v =@= (undefined :: Vector a) of -- | Converts the second NdArray to be the same DType as the first. matchDType :: NdArray -> NdArray -> NdArray -matchDType (NdArray _ v) nd = convertDTypeTo (vecType v) nd +matchDType (NdArray _ v) = convertDTypeTo (vecType v) {- Helper which checks that the array isn't larger than the shape contraints. If it is valid the Boolean in the pair will be true and the vector is returned. @@ -522,9 +576,7 @@ reshape r (NdArray s v) = if product s == product r -- Checks that the first shape is smaller or equal to the second. smallerShape :: [Integer] -> [Integer] -> Bool -smallerShape s r = - if length s > length s then False - else and $ zipWith (<=) s r +smallerShape s r = (length s <= length r) && (and $ zipWith (<=) s r) -- | Adds zero-rows to an array. Will error if you map to a smaller shape. -- >>> x = fromList [2,2] [1,2,3,4 :: Int] @@ -535,37 +587,42 @@ smallerShape s r = padShape :: [Integer] -> NdArray -> NdArray padShape r (NdArray s v) = let - nullVec = V.replicate (size r) (indentityElem' v) + nullVec = V.replicate (size r) (identityElem v) newIndices = V.imap (\i _ -> fromIntegral $ map1DIndex s r (toInteger i) :: Int) v in if smallerShape s r then NdArray r (V.unsafeUpdate_ nullVec newIndices v) else error "Cannot map to a smaller shape." +-- | Truncates the array to be no larger than the specified dimensions. constrainShape :: [Integer] -> NdArray -> NdArray constrainShape r (NdArray s v) = let s' = zipWith min r s sPad = s' ++ replicate (length s - length r) 1 - in NdArray s' $ + in NdArray s' $ V.ifilter (\i _ -> and $ zipWith (<) (expandInd s (toInteger i)) sPad) v ---broadcast :: forall a . DType a => (NdArray, NdArray) -> Maybe (Vector a, Vector a) +-- | Takes a pair of NdArrays and attempts to copy slices so that they are size matched. +-- Arrays are broadcastable if they either match in corresponding dimensions or one is +-- of dimension size 1 e.g. [2,5,1] and [2,1,6]. Missing dimensions are padded with 1s +-- e.g. [1,2,3] and [3] are broadcastable. broadcast :: (NdArray, NdArray) -> Maybe (NdArray, NdArray) -broadcast ((NdArray s v), (NdArray r u)) = +broadcast (NdArray s v, NdArray r u) = let (s',v',r',u') = broadcastDimensions s v r u - newshape = sequenceA $ zipWith (\x y -> if x == y || x == 1 || y == 1 + newshape = zipWithM (\x y -> if x == y || x == 1 || y == 1 then Just (max x y) else Nothing) s' r' in case newshape of Nothing -> Nothing Just ns -> Just ( - NdArray ns $ padRepeats ns m s' v', --NdArray ns $ - NdArray ns $ padRepeats ns m r' u') -- NdArray ns $ - where m = (fst $ mapIndicies ns) + NdArray ns $ padRepeats ns m s' v', + NdArray ns $ padRepeats ns m r' u') + where m = fst $ mapIndicies ns --- makes the number of dimensions correct but not the size of the later 1s +-- Pads out dimensions for broadcasting if one array is dimensionally smaller than another. +-- e.g. [1,2,3] and [3]. broadcastDimensions :: (DType a, DType b) => [Integer] -> Vector a -> [Integer] -> Vector b -> ([Integer], Vector a, [Integer], Vector b) @@ -574,9 +631,9 @@ broadcastDimensions s v r u r,u) | sl > rl = (s,v, sdiff ++ r, - V.concat $ replicate (fromIntegral $ product $ sdiff) u) + V.concat $ replicate (fromIntegral $ product sdiff) u) | sl < rl = (rdiff ++ s, - V.concat $ replicate (fromIntegral $ product $ rdiff) v, + V.concat $ replicate (fromIntegral $ product rdiff) v, r,u) where sl = length s @@ -585,6 +642,8 @@ broadcastDimensions s v r u sdiff = take diff s rdiff = take diff r +-- Pads out a newshape with repetitions of the existing values +-- Takes the newshape, its map, the old shape and the vector. padRepeats :: DType a => [Integer] -> M.Map Int [Integer] -> [Integer] -> Vector a -> Vector a padRepeats newshape oneDmap s v = @@ -596,115 +655,61 @@ padRepeats newshape oneDmap s v = flatWrap = multiMap M.! multiWrap -- collapse the index over the vector in v V.! flatWrap) -{- -identifyCommon :: forall a . Eq a => [[a]] -> Maybe [(Int, a)] -identifyCommon [] = Nothing -identifyCommon (x : xs) = - let - n = length x - indexed = traverse (\y -> if length y == n then Just (zip [(0::Int)..] y) else Nothing) (x:xs) :: Maybe [[(Int, a)]] - common = fmap (foldr intersect (head $ fromJust indexed)) indexed - in - case common of - Just c | length c < n-1 -> Nothing - otherwise -> common --} - --- Concatenate a list of tensors into a single tensor. All input tensors must have the --- same shape, except for the dimension size of the axis to concatenate on. +-- | Concatenate a list of tensors into a single tensor. All input tensors must have the +-- same shape, except for the dimension size of the axis to concatenate on. +-- Returns Nothing if the arrays are not all of the same type or matching shapes. concatAlong :: Int -> [NdArray] -> Maybe NdArray concatAlong _ [] = Nothing concatAlong _ [nd] = Just nd concatAlong axis ((NdArray s v):nds) = - case extractVectors ((NdArray s v):nds) (vecType v) of + case extractVectors (NdArray s v : nds) (vecType v) of Nothing -> Nothing Just vs -> - case concatAlongVec vs (map shape ((NdArray s v):nds)) axis of + case concatAlongVec vs (map shape (NdArray s v : nds)) axis of Nothing -> Nothing Just (ns, c) -> Just $ NdArray ns c -{- Converts the dimension of each sub-array at the axis to a mapping from -an index along this axis in the new array to the sub array it corresponds to.-} ---plotArrays :: [Integer] -> ---plotArrays dims = --- concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim]]) [0..] dims - ---concatenateAlong nds axis = case identifyCommon shapes of -{- -concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) -concatAlongVec vs shs axis = case identifyCommon shs of - Nothing -> Nothing - Just c -> - if 0 <= axis && axis < n - then if (length c == n) || (length c == n-1 && not (elem axis (map fst c))) - then - let - concatIndicies = map (!! axis) shs -- axis can actually be concatenated along - concatSize = sum concatIndicies - steps = [0] ++ scanl1 (+) concatIndicies - ranges = zip steps (drop 1 steps) - fullranges = map (\(x,y) -> [x..y-1]) ranges - numbered = M.fromList $ concat $ zipWith (\x y -> map (\z-> (z, y)) x) fullranges [0..] - newshape = replaceNth axis concatSize base - (oneDmap,_) = mapIndicies newshape - multiMaps = map (\x -> snd $ mapIndicies x) shs - in - --in Just newshape - Just (newshape, V.generate (fromIntegral $ product newshape) (\i -> - let - multiI = oneDmap M.! i - array = numbered M.! (multiI !! axis) - arraySize = concatIndicies !! array - arrayMultiI = replaceNth axis (multiI !! axis - (steps !! array)) multiI - arrayFlatI = (multiMaps !! array) M.! arrayMultiI - in - --arrayFlatI - (vs !! array) V.! arrayFlatI <-@ typeRep @(a) - ) ) - - else Nothing - else Nothing - where - base = head shs - n = length $ base --} - +-- Helper for concatenation of vectors and their associated shapes. concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) concatAlongVec vs shs axis = if not (checkShapeLengths shs) || not (checkAxis axis shs) then Nothing else - let + let + -- Calculates the newshape by adding up all the dimensions along the axis axDim = axisDimensions axis shs newshape = replaceNth axis (Prelude.sum axDim) (head shs) - -- first is sub-array number, second is sub-array index - arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim + -- Each array to be concatenated is given a number to index it with + -- Values are indexed by array number, then by position in the array + arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim (newMultiInds, _) = mapIndicies newshape - subArrayMaps = map (\x -> snd $ mapIndicies x) shs + subArrayMaps = map (snd . mapIndicies) shs in Just (newshape, V.generate (length newMultiInds) (\i -> let + -- Generating the new vector by converting the new flat index to a multi-index + -- then mapping it to a sub-array and index and reading the value. multiI = newMultiInds M.! i - (arrayNo, arrayAxInd) = arrayPlot !! (fromIntegral $ multiI !! axis) + (arrayNo, arrayAxInd) = arrayPlot !! fromIntegral (multiI !! axis) array = vs !! arrayNo arrayMap = subArrayMaps !! arrayNo arrayMultiI = replaceNth axis arrayAxInd multiI in - --arrayNo - vecInd arrayMap array arrayMultiI <-@ typeRep @(a) + vecInd arrayMap array arrayMultiI <-@ typeRep @a ) ) +-- Swaps in a value at the given index replaceNth :: Int -> a -> [a] -> [a] replaceNth n x l = take n l ++ [x] ++ drop (n+1) l --- same number of dimensions +-- Checks for the same number of dimensions checkShapeLengths :: [[Integer]] -> Bool checkShapeLengths [] = False checkShapeLengths shapes = (filter (\sh -> length sh /= baseLen) shapes) == [] where baseLen = length $ head shapes --- dimensions are the same save perhaps the axis one +-- Checks that each dimension is the same save perhaps the axis one checkAxis :: Int -> [[Integer]] -> Bool checkAxis _ [] = False checkAxis axis shapes = @@ -713,32 +718,23 @@ checkAxis axis shapes = base = head dropAxis in 0 <= axis && axis <= length base && (foldr intersect base dropAxis) == base --- gets the size of the dimension of the axis over all the shapes +-- Gets the size of the dimension of the axis over all the shapes axisDimensions :: Int -> [[Integer]] -> [Integer] axisDimensions axis shapes = map (!! axis) shapes ---ctest = concatAlongVec [V.fromList [1..6::Int], V.fromList [11..16::Int], V.fromList [101..108::Int]] [[2,3], [2,3], [2,4]] 1 ---dtest = concatAlong [fromList [2,3] [1..6::Int], fromList [2,3] [11..16::Int], fromList [2,4] [101..108::Int]] 1 - +-- | Takes an array, set of sub-indicies and axis and repeatedly takes slices +-- of the array restricted to that index along the specified axis. +-- The slices are then concatenated into the final array. gather :: NdArray -> [Integer] -> Integer -> NdArray gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) where ax = fromIntegral axis sliceLead = replicate ax (0,-1) - --(m,_) = mapIndicies $ shape nd - -{- -onnxex = fromMatrix $ A [ - A [B (1.0::Float), B 1.2, B 1.9], - A [B 2.3, B 3.4, B 3.9], - A [B 4.5, B 5.7, B 5.9]] - -etest = gather onnxex [0,2] 1 --} -- * Matrix Operations +-------------------------------------------------------------------------------- --- ROWS, COLUMNS & DIAGONALS +-- * Rows, Columns and Diagonals {- | Switches the rows at the two given indicies over. NB: designed for 2x2 matricies so will only make swaps in the 'front' matrix of a tensor. @@ -776,7 +772,7 @@ diagonalVec s v = rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) --- TRANSPOSITION +-- * Transposition -- | Reverses the order of axes and switches the elements accordingly. transpose :: NdArray -> NdArray @@ -810,13 +806,16 @@ permuteList perm l = if sort perm /= [0 .. length l -1] invertPermutation :: [Int] -> [Int] invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -1] --- MULTIPLICATION +-- * Multiplication -- | Dot product over matricies of the same shape. dot :: DType a => NdArray -> NdArray -> a dot nd1 nd2 = foldrA (DType.add) (DType.addId) (nd1*nd2) --- +-- | Standard matrix multiplication following NumPy conventions. +-- 1D arrays have the extra dimension pre/appended +-- 2D arrays are multiplied as expected +-- ND-arrays are broadcast to match each other where possible and treated as stacks of nxm/pxq arrays. matMul :: NdArray -> NdArray -> NdArray matMul (NdArray s v) (NdArray r u) = case v =@= u of @@ -829,23 +828,24 @@ matMul (NdArray s v) (NdArray r u) = ([m, n], [p]) | m == p -> NdArray [n] (matMulVec s v [p,1] u) -- ND-arrays are broadcast to match each other where possible and treated as -- stacks of nxm/pxq arrays. - ((m : n : ss), (q : p : rs)) | m == p -> + ((m : n : _), (q : p : _)) | m == p -> let - (s', v', r', u') = broadcastDimensions s v r u + (s', v', _r', u') = broadcastDimensions s v r u stackA = vectorChunksOf (fromIntegral @Integer @Int $ m * n) v' stackB = vectorChunksOf (fromIntegral @Integer @Int $ q * p) u' stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,q]) stackB in NdArray (take (length s' -2) s' ++ [n,q]) $ V.concat stackAB - _ -> error "Invalid matrix dimensions for multiplication" - _ -> error "Cannot multiply matricies of two distinct types." + _ -> throw (ShapeMismatch (NdArray s v) (NdArray r u) "matMul") + _ -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "matMul") +-- Splits a vector into a list of vectors of the given size. vectorChunksOf :: V.Storable a => Int -> Vector a -> [Vector a] vectorChunksOf _ v | V.null v = [] vectorChunksOf n v = first : (vectorChunksOf n rest) where (first, rest) = V.splitAt n v --- returning the vector result of the matMul +-- Returning the vector result of the standard nxm matMul matMulVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector a -> Vector a matMulVec s v r u = @@ -858,16 +858,14 @@ matMulVec s v r u = in V.generate sz (matMulElem map1 map2 ks . (M.!) oneDkey) --- element at position [i,j] in the resultant nxp matrix (from matMultiplying a prev: mxn and pxm = pxn) ---matMulElem :: DType a => --- NdArray -> M.Map [Integer] Int -> NdArray -> M.Map [Integer] Int -> [Integer] -> a +-- Calculates the element at position [i,j] in the resultant nxp matrix of a matMul matMulElem :: forall a . DType a => ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a matMulElem map1 map2 ks (i:j:_) = foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks matMulElem _ _ _ _ = DType.multId :: a --- DETERMINANTS & INVERSES +-- * Determinants and Inverses -- | Converts a nxn matrix to upper triangle form. O(n^3). upperTriangle :: NdArray -> NdArray @@ -877,7 +875,7 @@ upperTriangle (NdArray (c:rs) v) = (_, fromMulti) = mapIndicies (c:rs) traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] in - NdArray (c:rs) $ triangulateVec fromMulti v traversals (indentityElem' v) + NdArray (c:rs) $ triangulateVec fromMulti v traversals (identityElem v) -- Upper triangle form on the hidden vector. triangulateVec :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> a -> Vector a @@ -964,7 +962,7 @@ swapRowsWith0Pivot :: NdArray -> Maybe NdArray swapRowsWith0Pivot (NdArray s v) = let diag = diagonalVec s v - ident = indentityElem' diag + ident = identityElem diag in case V.elemIndex ident diag of -- x is the column-index of the 0 pivot @@ -986,7 +984,16 @@ frontColumn col s v = V.ifilter rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) --- NB if the matricies are integers the scalars will also become integers so you should convert the matricies first +{- | General matrix multiplication. Calculates alpha*AB + beta*C with the option +to transpose A and B first. +Takes A, B, C, A transpose?, B transpose?, alpha, beta +Returns nothing if the matrix types/sizes do not match. +Will attempt to broadcast the shape of C and convert the types of alpha & beta. + +For more information see: +https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms#Level_3 +NB: if the matricies are integers the scalars will also become integers so you should convert the matricies first +-} gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe (NdArray) gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = @@ -1016,14 +1023,7 @@ gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = -- Finally, combine the two Just $ (alphaAB + betaC) -{- -Ok so we need to convert the scalars to whatever the matrix types are -and check matrix types all match -and check the a and b shapes are good -and possibly size c up -and scale the alphas & betas --} - +-- Transpose the shape-vector pair if the boolean is true, otherwise return the original. applyTransposition :: forall a . DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) applyTransposition (s, v) b = let @@ -1063,12 +1063,6 @@ gemmTyping vA vB vC alpha beta = _ -> Nothing --- * Common Errors -shapeMismatch :: String -> String -> String -shapeMismatch s1 s2 = "Cannot match first array of shape '" <> s1 <> "' with array of shape '" <> s2 <> "'." - -typeMismatch :: String -> String -> String -typeMismatch t1 t2 = "Cannot match first array of type '" <> t1 <> "' with array of type '" <> t2 <> "'." ndt1 :: NdArray ndt1 = fromList [3,2] [1,2,3,4,5,6::Int] @@ -1077,14 +1071,4 @@ ndt2 = fromList [2,3] [0,2,4,6,8,10::Int] nd3 = fromList [2,2] [1,2,3,4 :: Int] -nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] --- det = -71 -{- - 1 0 0 2 5 1 -9/2 1 0 x 0 s 5/2 - 2 t 1 0 0 71/41 - -t = -12/41 -s = -41/2 --} - +nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] \ No newline at end of file diff --git a/test/Test/aikens_out.txt b/test/Test/aikens_out.txt deleted file mode 100644 index 5666a0a..0000000 --- a/test/Test/aikens_out.txt +++ /dev/null @@ -1,195 +0,0 @@ -"Constant: IntScalar (Array V0 [3])" -"Input: FloatMatrix (Array (V2 500 12) [1.7640524,0.4001572,0.978738,2.2408931,1.867558,-0.9772779,0.9500884" -"Constant: IntScalar (Array V0 [9])" -"Gather: FloatMatrix (Array (V2 500 1) [0.41059852,0.6536186,-1.9807965,-0.4380743,0.3024719,5.1945396e-2,0.9" -"Constant: FloatMatrix (Array (V2 500 15) [5.0318122e-2,0.8964633,0.15310746,0.3642171,0.8965451,0.76894027,0.2" -"Constant: FloatMatrix (Array (V2 15 500) [4.968053e-2,0.57025206,0.45921576,0.14775777,0.25258988,0.44144374,0" -"Constant: IntScalar (Array V0 [0])" -"Gather: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" -"Constant: FloatMatrix (Array (V2 1 500) [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.117258" -/ -"Constant: IntScalar (Array V0 [2])" -"Gather: FloatMatrix (Array (V2 500 1) [0.978738,0.44386324,4.5758516e-2,-0.3873268,-0.89546657,-0.8131463,0." -"MatMul: Array (V2 1 1) [4.6461363]" -"Constant: FloatMatrix (Array (V2 500 1) [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.117258" -"MatMul: Array (V2 500 1) [3.0431468,3.7489762,4.526732,3.4945245,4.5740833,1.527907,0.5448006,9.191737e-2,2." -"Constant: FloatScalar (Array V0 [-1.0])" -"Constant: FloatMatrix (Array (V2 500 15) [0.28872812,0.22997248,0.82122374,0.21280658,9.241706e-2,5.1808298e-2" -"Constant: FloatMatrix (Array (V2 15 500) [5.8466375e-2,0.5863933,0.4988473,0.66705537,0.44786972,0.24729961,0." -"Constant: IntScalar (Array V0 [1])" -/ -"Gather: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" -"Constant: FloatMatrix (Array (V2 1 500) [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0." -"MatMul: Array (V2 1 1) [-12.392535]" -"Constant: FloatMatrix (Array (V2 500 1) [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0." -"MatMul: Array (V2 1 1) [-3.6630301]" -"MatMul: Array (V2 500 1) [-0.31687784,-0.55377394,-3.2807317,-0.9286113,-0.2849118,-1.044107,-0.73757833,-2." -"Identity: FloatScalar (Array V0 [-1.0])" -"Constant: FloatScalar (Array V0 [1.0])" -"MatMul: Array (V2 15 1) [-19.652634,-12.657694,-11.926977,-8.681829,-17.47995,-7.395108,-10.788708,-23.06547" -/ -"MatMul: Array (V2 500 1) [-75.00322,-115.5041,-96.75044,-84.42763,-76.216934,-101.877625,-122.852615,-90.928" -"Greater: BoolMatrix (Array (V2 500 1) [False,False,False,False,False,False,False,False,False,False,False,Fals" -"Cast: FloatMatrix (Array (V2 500 1) [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0" -"Mul: FloatMatrix (Array (V2 500 1) [-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0" -"LessOrEqual: BoolMatrix (Array (V2 500 1) [True,True,True,True,True,True,True,True,True,True,True,True,True,True," -"Cast: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1" -!!!! -"Mul: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" -"Add: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1" -"Clip: FloatMatrix (Array (V2 500 1) [0.4001572,0.121675014,-1.0,1.0,-0.21274029,-0.35955316,-1.0,1.0,1.0,1" - -"Constant: IntScalar (Array V0 [4])" -"Gather: FloatMatrix (Array (V2 500 1) [1.867558,1.4940791,1.5327792,-1.048553,-0.51080513,0.17742614,-0.8707" -"MatMul: Array (V2 1 1) [-5.5109644]" -"MatMul: Array (V2 500 1) [-3.6095958,-4.446808,-5.369334,-4.1449924,-5.4254994,-1.8123103,-0.6462093,-0.1090" -"Constant: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [-3.0])" -"Constant: FloatMatrix (Array (V2 500 1) [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.3589" -"Constant: FloatMatrix (Array (V2 1 500) [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.3589" -"MatMul: Array (V2 1 1) [7.183905]" -/ -"MatMul: Array (V2 500 1) [0.91656965,6.203072,5.8669558,4.4492354,5.271462,5.4725223,2.5784698,3.1083322,6.9" -"Identity: FloatScalar (Array V0 [-1.0])" -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [0.91656965,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1." -"MatMul: Array (V2 15 1) [-20.942812,-3.784609,-6.334184,-16.134716,-18.233606,-8.202613,-9.120655,-9.406257," -"MatMul: Array (V2 500 1) [-68.49755,-113.25019,-92.551636,-71.3755,-91.53658,-80.38321,-96.32032,-59.690422," -"LessOrEqual: BoolMatrix (Array (V2 500 1) [True,True,True,True,True,True,True,True,True,True,True,True,True,True," -"Cast: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1" -"Mul: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" - -"Greater: BoolMatrix (Array (V2 500 1) [False,False,False,False,False,False,False,False,False,False,False,Fals" -"Cast: FloatMatrix (Array (V2 500 1) [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0" -"Mul: FloatMatrix (Array (V2 500 1) [-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0" -"Add: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" -"Constant: FloatScalar (Array V0 [-5.0])" -"Identity: FloatScalar (Array V0 [-5.0])" -"Constant: FloatMatrix (Array (V2 500 5) [0.23994845,0.874964,0.63754064,0.72970045,0.88023925,0.37286758,0.826" -"Constant: FloatMatrix (Array (V2 5 500) [0.68415046,0.64249843,0.38679576,0.2793504,0.33298904,0.8958253,0.164" -"MatMul: Array (V2 5 1) [9.695124,8.50157,12.497513,1.4205827,0.6591711]" - -"MatMul: Array (V2 500 1) [19.3494,21.16829,15.6027155,12.495917,17.27474,21.631218,21.864082,16.089157,18.32" -"Constant: FloatScalar (Array V0 [5.0])" -"Identity: FloatScalar (Array V0 [5.0])" -"Clip: FloatMatrix (Array (V2 500 1) [5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5" -"Clip: FloatMatrix (Array (V2 500 1) [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394" -"Mul: FloatMatrix (Array (V2 500 1) [3.29447,1.1370505,3.4790328,-1.290025,0.82438725,-0.11931206,-0.99218" -"Mul: FloatMatrix (Array (V2 500 1) [0.7473168,0.1817921,-1.5327792,-1.048553,0.10866883,-6.379413e-2,0.87" -"Constant: FloatMatrix (Array (V2 500 15) [0.49574316,0.7819777,0.69376004,0.1799764,0.5237755,0.5874824,0.8836" -"Constant: FloatMatrix (Array (V2 15 500) [0.32731897,3.096807e-2,0.28492284,0.24839026,0.16009456,0.795056,2.3" - -"MatMul: Array (V2 15 1) [6.6918817,-2.6588368,0.11027944,4.9703884,2.4028583,3.8567388,-7.34609,-6.4442456e-" -"MatMul: Array (V2 500 1) [6.801699,18.984343,8.779206,-0.84524286,4.8614564,7.481981,9.997283,9.963657,-4.68" -"Identity: FloatScalar (Array V0 [-1.0])" -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,-0.84524286,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Mul: FloatMatrix (Array (V2 500 1) [1.867558,1.4940791,1.5327792,0.8862819,-0.51080513,0.17742614,-0.8707" -"Constant: FloatMatrix (Array (V2 500 15) [0.44727606,0.35552752,0.2695884,0.9328208,0.48338705,0.5175878,0.272" -"Constant: FloatMatrix (Array (V2 15 500) [0.3689707,0.58033675,0.2554481,0.8095724,0.3360446,0.3746431,0.60134" -"Gather: FloatMatrix (Array (V2 500 1) [2.2408931,0.33367434,-0.18718386,-0.30230275,0.3869025,-1.7262826,-0." - -"MatMul: Array (V2 15 1) [14.817481,24.485401,20.55075,10.031889,20.104307,8.246814,5.722944,10.099542,5.8898" -"MatMul: Array (V2 500 1) [102.732124,108.530594,84.816505,91.67203,97.957214,92.46261,140.92859,100.8252,126" -"Constant: FloatScalar (Array V0 [-10.0])" -"Constant: FloatScalar (Array V0 [10.0])" -"Clip: FloatMatrix (Array (V2 500 1) [10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0" -"Mul: FloatMatrix (Array (V2 500 1) [18.67558,14.940791,15.327792,-10.48553,-5.1080513,1.7742615,-8.707972" -"Constant: FloatMatrix (Array (V2 500 5) [0.11452955,0.32926255,0.7802916,0.4259823,0.7317075,0.8767155,0.69868" -"Constant: FloatMatrix (Array (V2 5 500) [0.23298955,7.8250706e-2,0.30304825,0.20618552,6.613237e-2,0.8574571,0" -"MatMul: Array (V2 5 1) [-11.041234,-14.194939,-2.4407754,-8.023029,-7.5566373]" - -"MatMul: Array (V2 500 1) [-16.789845,-32.091545,-12.810639,-20.871101,-27.662287,-28.113258,-24.279068,-25.5" -"Constant: FloatScalar (Array V0 [-0.5])" -"Constant: FloatScalar (Array V0 [0.5])" -"Clip: FloatMatrix (Array (V2 500 1) [-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5" -"Constant: FloatMatrix (Array (V2 500 5) [0.34180975,0.98490405,0.20374298,0.3445685,0.60249984,0.9405572,7.452" -"Constant: FloatMatrix (Array (V2 5 500) [0.586107,0.99137485,0.11356729,0.5807301,4.3787777e-2,0.8292314,0.730" -"MatMul: Array (V2 5 1) [-15.098518,-11.611204,-3.7695518,-0.9945117,-9.389298]" -"MatMul: Array (V2 500 1) [-23.364492,-25.763008,-21.12878,-25.39899,-20.12619,-16.109161,-18.095474,-16.7098" -"Constant: FloatScalar (Array V0 [-1.2])" - -"Constant: FloatScalar (Array V0 [1.2])" -"Clip: FloatMatrix (Array (V2 500 1) [-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2" -"MatMul: Array (V2 1 1) [-16.21508]" -"MatMul: Array (V2 500 1) [-2.068826,-14.001203,-13.24254,-10.042547,-11.898429,-12.35225,-5.8199673,-7.01594" -"Constant: FloatScalar (Array V0 [3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Identity: FloatScalar (Array V0 [-3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-3.0,-1.8123103,-0.6462093,-0.109026805,-3.0,-2.8" -"Mul: FloatMatrix (Array (V2 500 1) [-5.602674,-4.4822373,-4.5983377,3.145659,1.5324154,-0.32155123,0.5627" - -"Clip: FloatMatrix (Array (V2 500 1) [-2.068826,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0" -"Constant: IntScalar (Array V0 [5])" -"Gather: FloatMatrix (Array (V2 500 1) [-0.9772779,-0.20515826,1.4693588,-1.420018,-1.1806322,-0.40178093,-0." -"Constant: FloatMatrix (Array (V2 500 1) [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.66" -"Constant: FloatMatrix (Array (V2 1 500) [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.66" -"MatMul: Array (V2 1 1) [-12.646031]" -"MatMul: Array (V2 500 1) [-4.2586102,-11.211211,-8.299928,-4.0727386,-1.4304522,-3.8753371,-8.4568405,-9.278" -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-1.4304522,-3.0,-3.0,-3.0,-3.0,-1.7071048,-3.0,-3" -"Mul: FloatMatrix (Array (V2 500 1) [2.021818,0.6154748,-4.4080763,4.2600536,3.5418968,1.2053428,1.736549," -"MatMul: Array (V2 1 1) [-8.988207]" -"MatMul: Array (V2 500 1) [-3.0268207,-7.968404,-5.8991995,-2.8947117,-1.0166985,-2.7544081,-6.010726,-6.5947" -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-2.8947117,-1.0166985,-2.7544081,-3.0,-3.0,-3.0,-1.213" -"Mul: FloatMatrix (Array (V2 500 1) [2.9318337,0.6154748,-4.4080763,4.110543,1.200347,1.1066687,1.736549,-" -"Constant: FloatMatrix (Array (V2 500 1) [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62" -"Constant: FloatMatrix (Array (V2 1 500) [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62" -"MatMul: Array (V2 1 1) [-7.2386208]" -"MatMul: Array (V2 500 1) [-4.4753776,-5.4445796,-3.7490675,-0.9543859,-4.7584944,-0.6636876,-4.501772,-1.904" -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-0.9543859,-3.0,-0.6636876,-3.0,-1.9042522,-3.0,-0.550" -"MatMul: Array (V2 1 1) [-1.5463191]" -"MatMul: Array (V2 500 1) [-0.95603323,-1.1630749,-0.8008784,-0.20387655,-1.0165128,-0.1417774,-0.9616716,-0." -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-0.95603323,-1.1630749,-0.8008784,-0.20387655,-1.0165128,-0.1417774,-" -"Mul: FloatMatrix (Array (V2 500 1) [0.93431014,0.23861441,-1.1767777,0.28950837,1.2001277,5.6963455e-2,0." -"Mul: FloatMatrix (Array (V2 500 1) [2.9318337,0.6154748,-4.4080763,4.2600536,3.5418968,0.72815174,0.37405" -"MatMul: Array (V2 1 1) [-12.180219]" -"MatMul: Array (V2 500 1) [-1.554032,-10.517229,-9.947348,-7.543621,-8.937696,-9.278591,-4.3717623,-5.2701373" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-1.554032,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-2.8" -"MatMul: Array (V2 1 1) [-15.176804]" -"MatMul: Array (V2 500 1) [-9.94057,-12.246192,-14.786765,-11.415014,-14.941439,-4.990974,-1.7796144,-0.30025" -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-1.7796144,-0.30025205,-3.0,-7.8356236e" -"MatMul: Array (V2 500 1) [-1.0720413,-1.8734934,-11.099167,-3.1416197,-0.9638959,-3.5323575,-2.495329,-8.631" -"Identity: FloatScalar (Array V0 [-3.0])" -"Identity: FloatScalar (Array V0 [3.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-1.0720413,-1.8734934,-3.0,-3.0,-0.9638959,-3.0,-2.495329,-3.0,-3.0,-" -"MatMul: Array (V2 1 1) [-11.002842]" -"MatMul: Array (V2 500 1) [-3.7052586,-9.754458,-7.2214584,-3.5435383,-1.2445834,-3.3717868,-7.3579826,-8.072" -"Identity: FloatScalar (Array V0 [-1.0])" -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0" -"MatMul: Array (V2 1 1) [12.541454]" -"MatMul: Array (V2 500 1) [7.7539277,9.433143,6.495541,1.6535453,8.244449,1.1498886,7.799658,3.2992601,7.7421" -"Identity: FloatScalar (Array V0 [-1.0])" -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.95448756,1.0,0.16623473,1.0,1.0" -"Identity: FloatScalar (Array V0 [-1.0])" -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [1.0,1.0,1.0,1.0,1.0,1.0,0.5448006,9.191737e-2,1.0,2.3987513e-2,1.0,1." -"Identity: FloatScalar (Array V0 [1.0])" -"Clip: FloatMatrix (Array (V2 500 1) [-0.31687784,-0.55377394,-1.0,-0.9286113,-0.2849118,-1.0,-0.73757833,-" -"Constant: IntScalar (Array V0 [6])" -"Gather: FloatMatrix (Array (V2 500 1) [0.95008844,0.3130677,0.15494743,-1.7062702,-2.8182229e-2,-1.6301984,-" -"Constant: IntScalar (Array V0 [7])" -"Gather: FloatMatrix (Array (V2 500 1) [-0.1513572,-0.85409576,0.37816253,1.9507754,0.42833188,0.46278226,5.6" -"Constant: IntScalar (Array V0 [8])" -"Gather: FloatMatrix (Array (V2 500 1) [-0.10321885,-2.5529897,-0.88778573,-0.5096522,6.651722e-2,-0.9072984," -"Constant: IntScalar (Array V0 [10])" -"Gather: FloatMatrix (Array (V2 500 1) [0.14404356,0.8644362,-0.34791216,-1.2527953,-0.6343221,0.7290906,0.46" -"Constant: IntScalar (Array V0 [11])" -"Gather: FloatMatrix (Array (V2 500 1) [1.4542735,-0.742165,0.15634897,0.7774904,-0.36274117,0.12898292,-1.53" -"Concat: FloatMatrix (Array (V2 500 28) [3.29447,0.7473168,1.867558,18.67558,-0.5,-1.2,5.0,2.021818,2.9318337" -"Constant: FloatMatrix (Array (V2 1 28) [-0.18432815,-3.3549756e-2,0.1182383,-8.149214e-2,-0.17400299,3.5006218" -"Constant: FloatMatrix (Array (V2 1 1) [6.1723504e-2])" -"Gemm: FloatMatrix (Array (V2 500 1) [-2.557051,-1.4029355,-1.2166657,2.4757802,0.6224911,0.714476,2.072814" -"Input: FloatMatrix (Array (V2 500 1) [-2.557051,-1.4029355,-1.2166657,2.4757802,0.6224911,0.714476,2.072814" \ No newline at end of file diff --git a/test/Test/rowans_out.txt b/test/Test/rowans_out.txt deleted file mode 100644 index 8263ffa..0000000 --- a/test/Test/rowans_out.txt +++ /dev/null @@ -1,194 +0,0 @@ -"Constant: {elements: [3], shape: [1]}" -"Input: {elements: [1.7640524,0.4001572,0.978738,2.2408931,1.867558,-0.9772779,0.95008844,-0.1513572,-0.1032" -"Constant: {elements: [9], shape: [1]}" -"Gather: {elements: [0.41059852,0.6536186,-1.9807965,-0.4380743,0.3024719,5.1945396e-2,0.9008265,0.97663903,1" -"Constant: {elements: [5.0318122e-2,0.8964633,0.15310746,0.3642171,0.8965451,0.76894027,0.23978919,0.1712786,0." -"Constant: {elements: [4.968053e-2,0.57025206,0.45921576,0.14775777,0.25258988,0.44144374,0.826559,0.44754368,0" -"Constant: {elements: [0], shape: [1]}" -"Gather: {elements: [1.7640524,0.7610377,2.2697546,1.2302907,-1.6138978,-0.67246044,1.1394007,1.4882522,1.050" -"Constant: {elements: [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.11725885,1.9783616e-2,0.5" -/ -"Constant: {elements: [2], shape: [1]}" -"Gather: {elements: [0.978738,0.44386324,4.5758516e-2,-0.3873268,-0.89546657,-0.8131463,0.40234163,1.1787796," -"MatMul: {elements: [4.646127], shape: [1,1]}" -"Constant: {elements: [0.6549844,0.80690193,0.9743003,0.7521356,0.9844918,0.3288554,0.11725885,1.9783616e-2,0.5" -"MatMul: {elements: [3.043141,3.748969,4.5267234,3.4945176,4.5740743,1.527904,0.5447995,9.1917194e-2,2.772392" -"Constant: {elements: [-1.0], shape: []}" -"Constant: {elements: [0.28872812,0.22997248,0.82122374,0.21280658,9.241706e-2,5.1808298e-2,0.69124675,0.552448" -"Constant: {elements: [5.8466375e-2,0.5863933,0.4988473,0.66705537,0.44786972,0.24729961,0.5134718,0.24591672,5" -"Constant: {elements: [1], shape: [1]}" - -"Gather: {elements: [0.4001572,0.121675014,-1.4543657,1.2023798,-0.21274029,-0.35955316,-1.2348258,1.8958892," -"Constant: {elements: [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0.20135742,0.69653153" -"MatMul: {elements: [-12.392532], shape: [1,1]}" -"Constant: {elements: [8.650702e-2,0.1511792,0.8956333,0.25350904,7.7780366e-2,0.28503913,0.20135742,0.69653153" -"MatMul: {elements: [-3.6630313], shape: [1,1]}" -"MatMul: {elements: [-0.31687793,-0.5537741,-3.2807329,-0.9286116,-0.28491193,-1.0441072,-0.7375786,-2.551416" -"Identity: {elements: [-1.0], shape: []}" -"Constant: {elements: [1.0], shape: []}" -"MatMul: {elements: [-19.65264,-12.657692,-11.92697,-8.681826,-17.479946,-7.3951087,-10.7887125,-23.065464,-4" - -"MatMul: {elements: [-75.00323,-115.504105,-96.75044,-84.427635,-76.21692,-101.87762,-122.85262,-90.92825,-10" -"Greater: {elements: [False,False,False,False,False,False,False,False,False,False,False,False,False,False,Fals" -"Cast: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," -"Mul: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," -"LessOrEqual: {elements: [True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,Tru" -"Cast: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -!!!! -"Mul: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Add: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," - -"Constant: {elements: [4], shape: [1]}" -"Gather: {elements: [1.867558,1.4940791,1.5327792,-1.048553,-0.51080513,0.17742614,-0.87079716,-1.0707526,1.8" -"MatMul: {elements: [-5.5109715], shape: [1,1]}" -"MatMul: {elements: [-3.6096005,-4.4468136,-5.3693414,-4.1449976,-5.4255066,-1.8123127,-0.6462102,-0.10902694" -"Constant: {elements: [-3.0], shape: []}" -"Identity: {elements: [-3.0], shape: []}" -"Constant: {elements: [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.35892314,0.43268,0.9716" -"Constant: {elements: [0.12758654,0.863468,0.81668055,0.6193338,0.73378783,0.76177543,0.35892314,0.43268,0.9716" -"MatMul: {elements: [7.1839004], shape: [1,1]}" - -"MatMul: {elements: [0.916569,6.203068,5.8669515,4.449232,5.2714586,5.472519,2.578468,3.10833,6.9805555,6.504" -"Identity: {elements: [-1.0], shape: []}" -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [0.916569,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.5" -"MatMul: {elements: [-20.942808,-3.7846112,-6.33419,-16.134705,-18.2336,-8.202617,-9.120651,-9.4062605,-18.40" -"MatMul: {elements: [-68.49754,-113.25018,-92.55163,-71.37552,-91.5366,-80.383194,-96.32033,-59.690426,-74.75" -"LessOrEqual: {elements: [True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,True,Tru" -"Cast: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Mul: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," - -"Greater: {elements: [False,False,False,False,False,False,False,False,False,False,False,False,False,False,Fals" -"Cast: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," -"Mul: {elements: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0," -"Add: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Constant: {elements: [-5.0], shape: []}" -"Identity: {elements: [-5.0], shape: []}" -"Constant: {elements: [0.23994845,0.874964,0.63754064,0.72970045,0.88023925,0.37286758,0.8268966,0.74059576,0.8" -"Constant: {elements: [0.68415046,0.64249843,0.38679576,0.2793504,0.33298904,0.8958253,0.1640203,0.12165266,0.7" -"MatMul: {elements: [9.695128,8.501572,12.497501,1.4205788,0.6591735], shape: [5,1]}" - -"MatMul: {elements: [19.349392,21.168283,15.602718,12.49592,17.27473,21.631216,21.864082,16.089157,18.328959," -"Constant: {elements: [5.0], shape: []}" -"Identity: {elements: [5.0], shape: []}" -"Clip: {elements: [5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0," -"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0," -"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" -"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" -"Constant: {elements: [0.49574316,0.7819777,0.69376004,0.1799764,0.5237755,0.5874824,0.8836009,0.23305726,0.409" -"Constant: {elements: [0.32731897,3.096807e-2,0.28492284,0.24839026,0.16009456,0.795056,2.3362875e-2,0.9850323," - -"MatMul: {elements: [6.6918926,-2.6588376,0.11027652,4.97039,2.4028609,3.856735,-7.346094,-6.444438e-2,-4.226" -"MatMul: {elements: [6.8017015,18.984348,8.779214,-0.845247,4.861458,7.481983,9.9972925,9.963661,-4.686076,5." -"Identity: {elements: [-1.0], shape: []}" -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [1.0,1.0,1.0,-0.845247,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.16868544,1.0,1." -"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" -"Constant: {elements: [0.44727606,0.35552752,0.2695884,0.9328208,0.48338705,0.5175878,0.2729408,0.5210254,5.075" -"Constant: {elements: [0.3689707,0.58033675,0.2554481,0.8095724,0.3360446,0.3746431,0.6013461,0.58739585,0.9831" -"Gather: {elements: [2.2408931,0.33367434,-0.18718386,-0.30230275,0.3869025,-1.7262826,-0.6848101,-0.17992483" -"MatMul: {elements: [14.817481,24.48539,20.550753,10.031887,20.104303,8.246808,5.7229443,10.099546,5.8898077," -"MatMul: {elements: [102.7321,108.53057,84.81648,91.672005,97.9572,92.46259,140.92854,100.82519,126.107925,13" -"Constant: {elements: [-10.0], shape: []}" -"Constant: {elements: [10.0], shape: []}" -"Clip: {elements: [10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10.0,10." -"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" -"Constant: {elements: [0.11452955,0.32926255,0.7802916,0.4259823,0.7317075,0.8767155,0.6986809,0.8781359,0.6030" -"Constant: {elements: [0.23298955,7.8250706e-2,0.30304825,0.20618552,6.613237e-2,0.8574571,0.9918714,0.53765047" -"MatMul: {elements: [-11.04123,-14.194946,-2.440776,-8.023025,-7.5566387], shape: [5,1]}" -"MatMul: {elements: [-16.789845,-32.091545,-12.81064,-20.8711,-27.662294,-28.113256,-24.279064,-25.578613,-19" -"Constant: {elements: [-0.5], shape: []}" -"Constant: {elements: [0.5], shape: []}" -"Clip: {elements: [-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0." -"Constant: {elements: [0.34180975,0.98490405,0.20374298,0.3445685,0.60249984,0.9405572,7.452351e-2,0.6528334,0." -"Constant: {elements: [0.586107,0.99137485,0.11356729,0.5807301,4.3787777e-2,0.8292314,0.7302829,0.48619032,0.7" -"MatMul: {elements: [-15.098519,-11.611205,-3.7695527,-0.99450964,-9.389294], shape: [5,1]}" -"MatMul: {elements: [-23.364487,-25.763004,-21.128778,-25.39899,-20.12619,-16.109161,-18.095474,-16.709824,-2" -"Constant: {elements: [-1.2], shape: []}" -"Constant: {elements: [1.2], shape: []}" -"Clip: {elements: [-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1.2,-1." -"MatMul: {elements: [-16.215082], shape: [1,1]}" -"MatMul: {elements: [-2.0688262,-14.0012045,-13.242542,-10.042548,-11.89843,-12.352251,-5.819968,-7.015942,-1" -"Constant: {elements: [3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Identity: {elements: [-3.0], shape: []}" -"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-3.0,-1.8123127,-0.6462102,-0.109026946,-3.0,-2.8452566e-2,-3.0,-3.0" -"Mul: {elements: [3.487773,2.2322724,2.3494122,1.0994633,0.2609219,3.1480037e-2,0.75828767,1.1465112,3.546" -"Clip: {elements: [-2.0688262,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3" -"Constant: {elements: [5], shape: [1]}" -"Gather: {elements: [-0.9772779,-0.20515826,1.4693588,-1.420018,-1.1806322,-0.40178093,-0.5788497,1.0544517,-" -"Constant: {elements: [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.6687347,0.73371655,0." -"Constant: {elements: [0.33675468,0.8865399,0.65632665,0.32205665,0.113114715,0.3064469,0.6687347,0.73371655,0." -"MatMul: {elements: [-12.6460285], shape: [1,1]}" -"MatMul: {elements: [-4.2586093,-11.211208,-8.299926,-4.0727377,-1.4304519,-3.8753364,-8.456839,-9.278601,-8." -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-1.4304519,-3.0,-3.0,-3.0,-3.0,-1.7071044,-3.0,-3.0,-3.0,-3.0,-2.425" -"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" -"MatMul: {elements: [-8.988196], shape: [1,1]}" -"MatMul: {elements: [-3.026817,-7.9683943,-5.899193,-2.8947084,-1.0166973,-2.754405,-6.0107193,-6.5947886,-5." -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-3.0,-3.0,-3.0,-2.8947084,-1.0166973,-2.754405,-3.0,-3.0,-3.0,-1.2133287,-3.0,-3.0,-2.83" -"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" -"Constant: {elements: [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62191015,0.26306838,0" -"Constant: {elements: [0.61826384,0.75215703,0.5179257,0.13184637,0.6573758,9.168702e-2,0.62191015,0.26306838,0" -"MatMul: {elements: [-7.23862], shape: [1,1]}" -"MatMul: {elements: [-4.475377,-5.4445786,-3.749067,-0.95438576,-4.7584934,-0.6636875,-4.501771,-1.9042519,-4" -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-3.0,-3.0,-3.0,-0.95438576,-3.0,-0.6636875,-3.0,-1.9042519,-3.0,-0.5509068,-1.5594246,-9" -"MatMul: {elements: [-1.54632], shape: [1,1]}" -"MatMul: {elements: [-0.9560337,-1.1630754,-0.8008788,-0.20387667,-1.0165133,-0.14177747,-0.96167207,-0.40678" -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-0.9560337,-1.1630754,-0.8008788,-0.20387667,-1.0165133,-0.14177747,-0.96167207,-0.40678" -"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" -"Mul: {elements: [0.95507205,4.2089913e-2,2.1590152,2.016451,1.3938925,0.16142792,0.33506694,1.1118684,1.8" -"MatMul: {elements: [-12.180203], shape: [1,1]}" -"MatMul: {elements: [-1.5540301,-10.517216,-9.947335,-7.5436115,-8.937685,-9.27858,-4.371757,-5.2701306,-11.8" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-1.5540301,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-2.8304725,-3.0,-3.0,-" -"MatMul: {elements: [-15.17683], shape: [1,1]}" -"MatMul: {elements: [-9.940587,-12.246214,-14.786791,-11.415034,-14.941465,-4.9909825,-1.7796177,-0.3002526,-" -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-3.0,-3.0,-3.0,-3.0,-3.0,-3.0,-1.7796177,-0.3002526,-3.0,-7.835638e-2,-3.0,-3.0,-3.0,-3." -"MatMul: {elements: [-1.072041,-1.8734931,-11.099164,-3.141619,-0.9638957,-3.5323565,-2.4953284,-8.631789,-6." -"Identity: {elements: [-3.0], shape: []}" -"Identity: {elements: [3.0], shape: []}" -"Clip: {elements: [-1.072041,-1.8734931,-3.0,-3.0,-0.9638957,-3.0,-2.4953284,-3.0,-3.0,-3.0,-3.0,-3.0,-3.0," -"MatMul: {elements: [-11.002837], shape: [1,1]}" -"MatMul: {elements: [-3.705257,-9.754454,-7.221455,-3.543537,-1.2445828,-3.3717854,-7.3579793,-8.072964,-7.33" -"Identity: {elements: [-1.0], shape: []}" -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1." -"MatMul: {elements: [12.541454], shape: [1,1]}" -"MatMul: {elements: [7.7539277,9.433143,6.495541,1.6535453,8.244449,1.1498886,7.799658,3.2992601,7.742188,0.9" -"Identity: {elements: [-1.0], shape: []}" -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.95448756,1.0,0.16623473,1.0,1.0,1.0,1.0,1.0,0.4958" -"Identity: {elements: [-1.0], shape: []}" -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [1.0,1.0,1.0,1.0,1.0,1.0,0.5447995,9.1917194e-2,1.0,2.3987466e-2,1.0,1.0,0.9589323,1.0,0." -"Identity: {elements: [1.0], shape: []}" -"Clip: {elements: [-0.31687793,-0.5537741,-1.0,-0.9286116,-0.28491193,-1.0,-0.7375786,-1.0,-1.0,-1.0,-1.0,-" -"Constant: {elements: [6], shape: [1]}" -"Gather: {elements: [0.95008844,0.3130677,0.15494743,-1.7062702,-2.8182229e-2,-1.6301984,-0.31155252,-0.40317" -"Constant: {elements: [7], shape: [1]}" -"Gather: {elements: [-0.1513572,-0.85409576,0.37816253,1.9507754,0.42833188,0.46278226,5.616534e-2,1.222445,0" -"Constant: {elements: [8], shape: [1]}" -"Gather: {elements: [-0.10321885,-2.5529897,-0.88778573,-0.5096522,6.651722e-2,-0.9072984,-1.1651498,0.208274" -"Constant: {elements: [10], shape: [1]}" -"Gather: {elements: [0.14404356,0.8644362,-0.34791216,-1.2527953,-0.6343221,0.7290906,0.46566245,0.3563664,-0" -"Constant: {elements: [11], shape: [1]}" -"Gather: {elements: [1.4542735,-0.742165,0.15634897,0.7774904,-0.36274117,0.12898292,-1.5362437,0.7065732,-0." -"Rowan: concat" -"Concat: {elements: [3.487773,3.487773,3.487773,3.487773,-0.5,-1.2,5.0,0.95507205,0.95507205,0.95507205,0.955" -[500,28] -"Constant: {elements: [-0.18432815,-3.3549756e-2,0.1182383,-8.149214e-2,-0.17400299,3.5006218e-2,6.768849e-3,-5" -"Constant: {elements: [6.1723504e-2], shape: [1,1]}" -"Gemm: {elements: [0.39932898,0.6212738,1.3125417,1.1153207,0.658881,0.89349085,1.0576929,0.9657631,0.98187" -"Output: {elements: [0.39932898,0.6212738,1.3125417,1.1153207,0.658881,0.89349085,1.0576929,0.9657631,0.98187" -"Rowan: Asserting equality" \ No newline at end of file From 8ca2ed1006aa80d10519530e8f5d96f3b52ee08f Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 10 Aug 2023 11:15:05 +0100 Subject: [PATCH 69/90] hlinted everything --- src/DType.hs | 77 ++++--- src/Indexing.hs | 52 +++-- src/MatrixForm.hs | 44 ++-- src/NdArray.hs | 8 +- src/NdArrayException.hs | 7 + src/Numskull.hs | 496 ++++++++++++++++++++-------------------- src/Serialisation.hs | 107 +++------ src/Typing.hs | 5 +- 8 files changed, 401 insertions(+), 395 deletions(-) diff --git a/src/DType.hs b/src/DType.hs index 16ad06f..17ed766 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -9,11 +9,20 @@ import GHC.Float (float2Double) import Data.Int import Data.Char --- Basis for all pointwise operations +-- | All types storable within an NdArray must implement DType. +-- This defines some basic properties, mathematical operations and standards for conversion. class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where + -- | Additive identity addId :: a + -- | Multiplicative identity multId :: a - -- Numeric + -- | Standard numeric operations + -- NB: + -- divide preserves DType + -- div is specifically for integer division and returns an Int + -- pow preserves DType + -- power is for precision and uses Doubles + -- mod returns an Int add :: a -> a -> a subtract :: a -> a -> a multiply :: a -> a -> a @@ -21,6 +30,7 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where div :: a -> a -> Int power :: a -> Double -> Double pow :: a -> a -> a + -- Log base x of y log :: a -> a -> a mod :: a -> a -> Int abs :: a -> a @@ -31,11 +41,12 @@ class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where sin :: a -> a cos :: a -> a tan :: a -> a - -- Logical + -- | Most logical operations are simply defined in the numeric section on Booleans. + -- Invert is naturally defined as -x numerically and NOT x logically. invert :: a -> a shiftleft :: a -> a shiftright :: a -> a - -- Casting + -- | Dtypes are converted between via the intermediate type of rational dtypeToRational :: a -> Rational rationalToDtype :: Rational -> a @@ -46,7 +57,7 @@ instance DType Int where add x y = x + y subtract x y = x - y multiply x y = x * y - divide x y = P.div x y + divide = P.div div x y = (fromIntegral $ P.div x y) :: Int power x d = fromIntegral x ** d pow x y = x ^ y @@ -66,14 +77,13 @@ instance DType Int where invert x = -x shiftleft x = x * 2 shiftright x = x `P.div` 2 - -- (Conversions) + -- Conversion dtypeToRational = toRational rationalToDtype = P.floor . fromRational @Double roundIntFunc :: (Float -> Float) -> Int -> Int roundIntFunc f x = (round $ f $ fromIntegral @Int @Float x) :: Int --- Entirely copied really instance DType Int32 where addId = 0 multId = 1 @@ -81,7 +91,7 @@ instance DType Int32 where add x y = x + y subtract x y = x - y multiply x y = x * y - divide x y = P.div x y + divide = P.div div x y = fromIntegral @Int32 @Int $ P.div x y power x d = fromIntegral x ** d pow x y = x ^ y @@ -101,7 +111,7 @@ instance DType Int32 where invert x = -x shiftleft x = x * 2 shiftright x = x `P.div` 2 - -- (Conversions) + -- Conversion dtypeToRational = toRational rationalToDtype = P.floor . fromRational @Double @@ -112,7 +122,7 @@ instance DType Int64 where add x y = x + y subtract x y = x - y multiply x y = x * y - divide x y = P.div x y + divide = P.div div x y = fromIntegral @Int64 @Int $ P.div x y power x d = fromIntegral x ** d pow x y = x ^ y @@ -132,7 +142,7 @@ instance DType Int64 where invert x = -x shiftleft x = x * 2 shiftright x = x `P.div` 2 - -- (Conversions) + -- Conversion dtypeToRational = toRational rationalToDtype = P.floor . fromRational @Double @@ -147,7 +157,7 @@ instance DType Float where div x y = P.floor x `P.div` P.floor y power x d = float2Double x ** d pow x y = x ** y - log x y = logBase x y + log = logBase mod x y = P.floor x `P.mod` P.floor y abs = P.abs signum = P.signum @@ -176,7 +186,7 @@ instance DType Double where div x y = P.floor x `P.div` P.floor y power x d = x ** d pow x y = x ** y - log x y = logBase x y + log = logBase mod x y = P.floor x `P.mod` P.floor y abs = P.abs signum = P.signum @@ -194,23 +204,30 @@ instance DType Double where dtypeToRational = toRational rationalToDtype = fromRational @Double -instance DType Bool where +instance DType Bool where addId = False multId = True - -- Numeric + -- | Logical OR add x y = x || y - subtract x y = (x || y) && not (x && y) + -- | Logical NOR + subtract x y = not (x || y) + -- | Logical AND multiply x y = x && y + -- | Logical NAND divide x y = not (x && y) - div _ _ = 0 - power _x _d = undefined - pow x y = toEnum (fromEnum x ^ fromEnum y) - log _x _y = undefined - mod x y = fromEnum x `P.mod` fromEnum y + div x y = fromEnum $ DType.divide x y + -- | Numeric power + power x d = fromIntegral (fromEnum x) ** d + -- | Logical reverse implication + pow x y = not y || x + -- | Logical implication + log _x _y = not x || y + -- | Logical XOR + mod x y = (x || y) && not (x && y) abs _ = True signum = id ceil = id - floor = id + floor = id -- Trig (False = 0, True = 1 or /=0) sin False = False sin True = True @@ -219,7 +236,8 @@ instance DType Bool where tan False = False tan True = True -- Logical - invert x = not x + -- Logical NOT + invert = not shiftleft _ = False shiftright _ = False -- Conversions @@ -242,8 +260,11 @@ instance DType Char where log _ _ = undefined mod _ _ = undefined abs = undefined - signum c = if isAlpha c then if isUpper c then 'A' else 'a' - else if isDigit c then '0' else c + signum c + | isUpper c = 'A' + | isLower c = 'a' + | isDigit c = '0' + | otherwise = c ceil = toUpper floor = toLower -- Trig @@ -256,8 +277,4 @@ instance DType Char where shiftright x = chr $ ord x - 1 -- Conversion dtypeToRational = toRational . ord - rationalToDtype = chr . P.floor. fromRational @Double - ---instance ByteString where? - ---instance DType BFloat16 where? \ No newline at end of file + rationalToDtype = chr . P.floor. fromRational @Double \ No newline at end of file diff --git a/src/Indexing.hs b/src/Indexing.hs index 541f335..e512832 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -14,9 +14,6 @@ import Typing import qualified DType import DType (DType) -data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) - --- * Indexing & Slicing {- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the shape of the array, [sx,sy,sz,...], as follows: @@ -29,10 +26,16 @@ shape of the array, [sx,sy,sz,...], as follows: ... -} +-- * INDEXING + +-- | Generates the list of all multi-dimensional indicies for a given shape generateIndicies :: [Integer] -> [[Integer]] -generateIndicies = foldr (\x xs -> [ (i:t) | i <- [0..(x-1)], t <- xs]) [[]] --- foldr (\x xs -> [ (i:t) | i <- [0..x], t <- xs]) [[]] [2,3,2] +generateIndicies = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] +{- | Generates two maps to convert between the single dimension index of the +underlying vector and the multi-dimensional index of the NdArray and back, +given the NdArray shape. +-} mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) where @@ -40,15 +43,13 @@ mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) oneDkey = zip [0..] twoDinds twoDkey = zip twoDinds [0..] --- Unsafe: Indexes the vector with the multi-index using a mapping +-- Indexes a vector with an NdArray multi-index using a mapping (unsafe). vecInd :: forall a . DType a => M.Map [Integer] Int -> Vector a -> [Integer] -> a vecInd mapp v i = v V.! (mapp M.! i) ---vecInd mapp v i = case v =@= (undefined :: Vector a) of --- Just HRefl -> v V.! (mapp M.! i) -- | Converts a shape and multi-index to a 1D index. collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd sh indicies = collapseRun (reverse$sh) (reverse$indicies) 1 +collapseInd sh indicies = collapseRun (reverse sh) (reverse indicies) 1 -- Helper for collapseInd collapseRun :: [Integer] -> [Integer] -> Integer -> Integer @@ -58,7 +59,7 @@ collapseRun (s:ss) (x:xs) runSize = x*runSize + collapseRun ss xs (s*runSize) -- | Converts a shape and 1D index to a multi-index. expandInd :: [Integer] -> Integer -> [Integer] -expandInd sh i = reverse $ expandRun (reverse$sh) i 1 +expandInd sh i = reverse $ expandRun (reverse sh) i 1 -- Helper for expandInd expandRun :: [Integer] -> Integer -> Integer -> [Integer] @@ -70,9 +71,9 @@ expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer map1DIndex s r i = collapseInd r (expandInd s i) --- | Checks an index does not exceed the shape +-- | Checks an index does not exceed the shape. validIndex :: NdArray -> [Integer] -> Bool -validIndex (NdArray s _) i = (length i == length s) && (and $ zipWith lessAbs i s) +validIndex (NdArray s _) i = (length i == length s) && and (zipWith lessAbs i s) where lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) {- | Takes a multi-dimensional index and returns the value in the NdArray at that position. @@ -86,7 +87,7 @@ value for the array e.g. 0. To avoid this use !?. -- >>> m #! [50] :: Int -- 0 (#!) :: DType a => NdArray -> [Integer] -> a -(NdArray s v) #! i = case (NdArray s v) !? i of +(NdArray s v) #! i = case NdArray s v !? i of Just val -> val Nothing -> DType.addId :: DType a => a @@ -110,40 +111,49 @@ value for the array e.g. 0. To avoid this use !?. Nothing -> Nothing else Nothing +-- * SLICING + +-- | Type which allows you to provide only a single index or a range of indicies. +data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) + +-- | Integrated indexing and slicing. For each dimension you can provide either a single value +-- or a range of values where a slice will be taken. (#!+) :: NdArray -> [IndexRange] -> NdArray (#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) where (m,_) = mapIndicies sh +-- Converts an IndexRange to a range of indicies in the standard pair form. forceRange :: IndexRange -> (Integer, Integer) forceRange (I i) = (i,i) forceRange (R s t) = (s,t) +-- Converts negative indicies to their positive equivalents, counting back +-- from the end of the array (i.e. -1 is the last element). positiveInd :: (Ord a, Num a) => a -> a -> a positiveInd s i = if i < 0 then s+i else i ---(!?+) :: NdArray -> [IndexRange] -> Maybe NdArray ---(!?+) = undefined - {- | Takes a series of ranges corresponding to each dimension in the array and returns -the sub-array. -} +the sub-array. Indicies are inclusive and can be negative. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) where (m,_) = mapIndicies sh +-- | Equivalent slicing operator. (!/) :: NdArray -> [(Integer, Integer)] -> NdArray (!/) nd ss = slice ss nd --- helper +-- Takes a slice on an NdArray given the mapping from the vector index to NdArray index. +-- Iterates through each dimension of the slice one at a time. sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray sliceWithMap _ _ [] nd = nd -sliceWithMap _ d _ (NdArray sh v) | d >= length sh = (NdArray sh v) +sliceWithMap _ d _ (NdArray sh v) | d >= length sh = NdArray sh v sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ sliceDim s d m (NdArray sh v) --- inclusive, supports negatives +-- Takes a slice of an NdArray at a particular dimension. sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray sliceDim (x,y) d m (NdArray sh v) = - if d >= length sh then error "Given dimension does not exist in array." + if d >= length sh then throw (ExceededShape d sh) else NdArray (if y' < x' then [] else shrinkNth d (y'-x'+1) sh) (V.ifilter diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index 151cd8e..b673db6 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -1,20 +1,14 @@ -{-# LANGUAGE TypeApplications #-} - module MatrixForm where import NdArray import Data.Tree import qualified Data.Vector.Storable as V -data TreeMatrix a = B a | A [TreeMatrix a] - --- READING MATRICIES -matrixToTree :: TreeMatrix a -> Tree [a] -matrixToTree (B x) = Node [x] [] -matrixToTree (A xs) = Node [] (map matrixToTree xs) +-- * READING MATRICIES +{- | This type is specifically for pretty explicit definitions of NdArrays. +The A constructor is for Array - a set of values and B is the value. -- Example 2x3x2 -{- l :: TreeMatrix Int l = A [A [A [B 1, B 2], A [B 3, B 4], @@ -24,22 +18,35 @@ l = A [A [A [B 1, B 2], A [B 9, B 10], A [B 11, B 12]]] -} +data TreeMatrix a = B a | A [TreeMatrix a] --- Prelude.map Prelude.length $ levels $ treeify l'' --- dimension = next val/current val --- Prelude.zipWith div (Prelude.drop 1 x) x +-- Converts a TreeMatrix to a Tree of lists +matrixToTree :: TreeMatrix a -> Tree [a] +matrixToTree (B x) = Node [x] [] +matrixToTree (A xs) = Node [] (map matrixToTree xs) +-- Converts a Tree of lists to a single ordered list. flattenToList :: Tree [a] -> [a] flattenToList = concat . flatten +-- Calculates the shape of the NdArray corresponding to the Tree. treeShape :: Tree [a] -> [Integer] treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLen) levelLen where levelLen = map length $ levels t +-- Calculates the shape of the NdArray corresponding to the TreeMatrix. matrixShape :: TreeMatrix a -> [Integer] matrixShape = treeShape . matrixToTree --- WRITING MATRICIES +-- * WRITING MATRICIES + +-- | Prints out the pretty NdArray representation. +printArray :: NdArray -> IO () +printArray nd = putStr $ prettyShowArray nd + +-- | Converts an NdArray to its pretty representation. +-- Values along a row are separated whitespace. Along a column, newlines. +-- For higher dimensions, an additional newline is added to separate the nxm matricies. prettyShowArray :: NdArray -> String prettyShowArray (NdArray s v) = conc <> "\n" where @@ -50,12 +57,13 @@ prettyShowArray (NdArray s v) = conc <> "\n" lined = addNewlines newlines spaced conc = concatMap snd lined -printArray :: NdArray -> IO () -printArray nd = putStr $ prettyShowArray nd - +-- Separates values along a row by whitespace. padStringTo :: Int -> String -> String padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " +-- Separates columns and higher dimensions by newlines. addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] -addNewlines [] xs = xs -addNewlines (l:ls) xs = map (\(i,x) -> if i /= 0 && i `mod` l == 0 then (i, "\n"++x) else (i,x)) (addNewlines ls xs) \ No newline at end of file +addNewlines = foldr (\l -> + map (\(i, x) -> if i /= 0 && i `mod` l == 0 + then (i, "\n" ++ x) + else (i, x))) \ No newline at end of file diff --git a/src/NdArray.hs b/src/NdArray.hs index c0e4a16..8c13d89 100644 --- a/src/NdArray.hs +++ b/src/NdArray.hs @@ -6,12 +6,12 @@ import DType import Data.Vector.Storable -- * NdArray --- Todo: Should shapes be [Integer] or [Int] or maybe even another vector? --- | The core of this module. NdArrays can be of any type (a) and size/shape (list of dimensions) but these are --- hidden by the type. Both attributes can be inferred using the library constructors (TODO!). +-- | The core of this module. NdArrays can be of any DType a and size/shape (list of dimensions) +-- These are hidden by the type. data NdArray where NdArray :: DType a => [Integer] -> Vector a -> NdArray --- Todo: show in a nicer shapely form :) +-- | By default arrays are printed flat with the shape as metadata. +-- For a tidier representation, use printArray. instance Show NdArray where show (NdArray s v) = "{elements: " <> show v <> ", shape: " <> show s <> "}" \ No newline at end of file diff --git a/src/NdArrayException.hs b/src/NdArrayException.hs index 2a15314..071a407 100644 --- a/src/NdArrayException.hs +++ b/src/NdArrayException.hs @@ -11,11 +11,14 @@ import Data.Vector.Storable (Vector) import DType import NdArray +-- | The main type of exception thrown from Numskull functions when the user +-- tries to perform illegal operations given the size and shape of the array. data NdArrayException = DTypeMismatch NdArray NdArray String | ShapeMismatch NdArray NdArray String | CreationSize Integer [Integer] | TypeMismatch String + | ExceededShape Integer [Integer] instance Exception NdArrayException @@ -41,5 +44,9 @@ instance Show NdArrayException where show (TypeMismatch str) = str + show (ExceededShape dim sh) = + "Cannot index into dimension " <> show dim <> "in NdArray of shape " <> show sh <> "." + +-- Returns the string type of vector elements. showType :: forall a . DType a => Vector a -> String showType _ = show (typeRep @a) diff --git a/src/Numskull.hs b/src/Numskull.hs index 6e4db31..d2ee02a 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -1,9 +1,9 @@ +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE FlexibleContexts #-} module Numskull ( -- Metadata @@ -28,8 +28,8 @@ module Numskull ( , squareArr -- General mapping, folding & zipping - , foldrA - , mapA + , foldrA + , mapA , mapTransform , pointwiseZip , pointwiseBool @@ -37,24 +37,24 @@ module Numskull ( -- Summaries , origin - , maxElem - , minElem + , maxElem + , minElem -- Mathematical constant , scale , absA , signumA - , ceilA - , floorA - , sinA - , cosA + , ceilA + , floorA + , sinA + , cosA , tanA , invertA , shiftleftA , shiftrightA -- Mathematical pointwise - , elemDivide + , elemDivide , elemDiv , elemPow , elemPower @@ -66,25 +66,25 @@ module Numskull ( -- Type Conversions , convertDTypeTo - , matchDType + , matchDType -- Size conversions , resize -- Shape conversions/manipulations , reshape - , padShape + , padShape , constrainShape , broadcast , concatAlong , gather -- Matrix manipulation - , swapRows + , swapRows , diagonal , transpose , transposePerm - + --Matrix multiplication , dot , matMul @@ -95,6 +95,7 @@ module Numskull ( , gemm -- Indexing + , IndexRange , collapseInd , expandInd , map1DIndex @@ -112,23 +113,27 @@ module Numskull ( -- typing , (=@=) + -- numpy serialisation + , saveNpy + , loadNpy + ) where -import NdArray +import DType (DType) import qualified DType -import DType (DType) -import MatrixForm -import Indexing -import Typing -import NdArrayException - -import Control.Exception +import Indexing +import MatrixForm +import NdArray +import NdArrayException +import Typing + +import Control.Exception +import Data.List (elemIndex, intersect, sort, zipWith4) +import qualified Data.Map as M +import Data.Maybe (fromJust) +import Data.Vector.Storable (Vector) import qualified Data.Vector.Storable as V -import Data.Vector.Storable (Vector) -import Type.Reflection -import qualified Data.Map as M -import Data.Maybe (fromJust) -import Data.List (sort, elemIndex, intersect, zipWith4) +import Type.Reflection import Debug.Trace @@ -151,14 +156,14 @@ instance Eq NdArray where Nothing -> True instance Ord NdArray where - {- | Arrays are only comparable when they are the same shape. Then they are + {- | Arrays are only comparable when they are the same shape. Then they are ordered by pointwise comparison. -} (NdArray s v) `compare` (NdArray r u) = if s == r then case v =@= u of Just HRefl -> compare v u Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "compare") else throw (ShapeMismatch (NdArray s v) (NdArray r u) "compare") - + (NdArray s v) <= (NdArray r u) = if s == r then case v =@= u of Just HRefl -> v <= u Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "'<='") @@ -177,7 +182,7 @@ instance Num NdArray where abs (NdArray s v) = NdArray s (V.map DType.abs v) -- | Signum of each element signum (NdArray s v) = NdArray s (V.map DType.signum v) - -- Creates a singleton array. NB: must be converted to a storable Int. + -- Creates a singleton array. NB: must be converted to a storable Int. fromInteger = singleton . fromInteger @Int -- * General & Creation @@ -189,7 +194,7 @@ instance Num NdArray where size :: [Integer] -> Int size sh = (fromIntegral $ product sh) :: Int --- | Returns the shape list of an array. +-- | Returns the shape list of an array. shape :: NdArray -> [Integer] shape (NdArray s _) = s @@ -203,9 +208,9 @@ ndType (NdArray _ v) = show $ vecType v -- | Compares the type of the array elements to the given TypeRep. checkNdType :: forall a b . (DType a, DType b) => NdArray -> TypeRep a -> Maybe (a :~~: b) -checkNdType (NdArray _ v) _ = - let tv = vecType v - in case eqTypeRep tv (typeRep @b) of +checkNdType (NdArray _ v) _ = + let tv = vecType v + in case eqTypeRep tv (typeRep @b) of Just HRefl -> eqTypeRep (typeRep @a) (tv :: TypeRep b) _ -> error "Impossibly mismatching types." @@ -220,10 +225,10 @@ isEmpty (NdArray _ v) = V.null v -- | Convert a list of arrays to a list of vectors, provided they are all of the specified type. extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] extractVectors [] _ = Just [] -extractVectors ((NdArray _ v) : nds) t = +extractVectors ((NdArray _ v) : nds) t = case v =@= (undefined :: Vector a) of Just HRefl -> - case extractVectors nds t of + case extractVectors nds t of Just vs -> Just (v:vs) _ -> Nothing Nothing -> Nothing @@ -234,10 +239,10 @@ identityElem _ = DType.addId :: DType a => a -- | Creates an NdArray from a given shape and list. The number of elements must match. -- >>> printArray $ fromList [2,2] [1,2,3,4::Int] --- 1 2 --- 3 4 +-- 1 2 +-- 3 4 fromList :: DType a => [Integer] -> [a] -> NdArray -fromList sh l = +fromList sh l = if length l /= size sh then throw $ CreationSize (fromIntegral $ length l) sh else NdArray sh (V.fromList l) @@ -253,17 +258,17 @@ fromListFlat l = NdArray [toInteger$length l] (V.fromList l) -- >>> A [B 3, B 4], -- >>> A [B 5, B 6]] -- >>> printArray $ fromMatrix m --- 1 2 --- 3 4 --- 5 6 +-- 1 2 +-- 3 4 +-- 5 6 fromMatrix :: DType a => TreeMatrix a -> NdArray fromMatrix m = NdArray (matrixShape m) (V.fromList l) where l = flattenToList $ matrixToTree m --- | The safe standard constructor. Returns Nothing if the --- shape does not match the given vector length. +-- | The safe standard constructor. Returns Nothing if the +-- shape does not match the given vector length. fromVector :: DType a => [Integer] -> Vector a -> Maybe NdArray -fromVector sh v = if V.length v == fromIntegral (product sh) +fromVector sh v = if V.length v == fromIntegral (product sh) then Just $ NdArray sh v else Nothing @@ -275,17 +280,17 @@ singleton x = NdArray [1] (V.fromList [x]) -- | Creates a flat array over the specified range. arange :: (Enum a, DType a) => a -> a -> NdArray -arange mini maxi = - if mini <= maxi +arange mini maxi = + if mini <= maxi then NdArray [fromIntegral $ fromEnum maxi - fromEnum mini + 1] $ V.fromList [mini..maxi] else NdArray [] (V.fromList [] :: Vector Int) -{- | Creates the smallest possible square matrix from the given list, +{- | Creates the smallest possible square matrix from the given list, padding out any required space with the identity element for the DType -} -squareArr :: forall a . DType a => [a] -> NdArray +squareArr :: forall a . DType a => [a] -> NdArray squareArr [] = NdArray [] (V.fromList [] :: Vector Int) -squareArr xs = - let +squareArr xs = + let l = length xs d = ceiling (sqrt $ fromIntegral @Int @Float l) d' = fromIntegral @Int @Integer d @@ -297,25 +302,25 @@ zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray zeros _ s = NdArray s zerovec where ident = DType.addId :: (DType a => a) - zerovec = (V.replicate (size s) ident) :: DType a => Vector a + zerovec = V.replicate (size s) ident :: DType a => Vector a -- * Pointwise Functions -------------------------------------------------------------------------------- -- * One Argument -{- | Near identical to a standard foldr instance, expect NdArrays do not have an explicit type. +{- | Near identical to a standard foldr instance, expect NdArrays do not have an explicit type. Folds in row-major order. -} foldrA :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b -foldrA f z (NdArray _ v) = +foldrA f z (NdArray _ v) = case v =@= (undefined :: Vector a) of Just HRefl -> V.foldr f z v _ -> throw $ TypeMismatch "Fold starting value type does not match array type." -- | Near identical to a standard map implementation in row-major order. mapA :: forall a . forall b . (DType a, DType b) => (a -> b) -> NdArray -> NdArray -mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of +mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of Just HRefl -> NdArray s (V.map f v) _ -> throw $ TypeMismatch "Map function input does not match array type." @@ -325,15 +330,15 @@ mapTransform f (NdArray s v) = NdArray s (V.map f v) -- | Multiplies all elements by a scalar. scale :: forall a . DType a => a -> NdArray -> NdArray -scale x = mapA DType.multiply x +scale = mapA DType.multiply -- | Takes the absolute value of all elements. absA :: NdArray -> NdArray absA = mapTransform DType.abs --- | Replaces all elements by their signum. +-- | Replaces all elements by their signum. -- >>> printArray $ signumA (fromList [5] [-50, -25, 0, 1, 10::Int]) --- -1 -1 0 1 1 +-- -1 -1 0 1 1 signumA :: NdArray -> NdArray signumA = mapTransform DType.signum @@ -381,38 +386,38 @@ maxElem nd = foldrA max (origin nd) nd minElem :: forall a . DType a => NdArray -> a minElem nd = foldrA min (origin nd) nd --- | Constrains all elements of the array to the range specified by [mini, maxi]. +-- | Constrains all elements of the array to the range specified by [mini, maxi]. -- If they are given as Nothing, the range is infinite in that direction. -- NB: must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd clip :: forall a . DType a => Maybe a -> Maybe a -> NdArray -> NdArray clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of - Just HRefl -> + Just HRefl -> case (mini, maxi) of (Just mn, Just mx) -> mapA (\x -> if x <= mn then mn else if x >= mx then mx else x) (NdArray s v) (Just mn, Nothing) -> mapA (\x -> if x <= mn then mn else x) (NdArray s v) (Nothing, Just mx) -> mapA (\x -> if x >= mx then mx else x) (NdArray s v) - (Nothing, Nothing) -> (NdArray s v) + (Nothing, Nothing) -> NdArray s v _ -> throw (TypeMismatch $ "Min and max types do not match array type of " <> show (vecType v) <> ".") -- * Two Arguments --- | The generic function for operating on two matching DType arrays with the same shape +-- | The generic function for operating on two matching DType arrays with the same shape -- in an element-wise/pointwise way. Errors if mismatching -- >>> x = fromList [2,2] [1,2,3,4 :: Int] -- >>> y = fromList [2,2] [5,2,2,2 :: Int] -- >>> printArray $ pointwiseZip (DType.multiply) x y --- 5 4 +-- 5 4 -- 6 8 pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray -pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then +pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> NdArray s (V.zipWith zipfunc v u) + Just HRefl -> NdArray s (V.zipWith zipfunc v u) Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") else throw (ShapeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") -- | A slightly specialised version of pointwise zip intended for comparative functions. pointwiseBool :: (forall t . DType t => t -> t -> Bool) -> NdArray -> NdArray -> NdArray -pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then +pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then case v =@= u of Just HRefl -> NdArray s (V.zipWith zipfunc v u) Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") @@ -421,7 +426,7 @@ pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then -- | Completely generic zip on two NdArrays. If the shapes mismatch, they are truncated as with -- standard zips. Function inputs must match the DTypes. zipArrayWith :: forall a b c . (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray -zipArrayWith zipfunc (NdArray s v) (NdArray r u) = +zipArrayWith zipfunc (NdArray s v) (NdArray r u) = let -- Truncate the shapes to match each other ndC1 = constrainShape r (NdArray s v) @@ -431,7 +436,7 @@ zipArrayWith zipfunc (NdArray s v) (NdArray r u) = -- Type check the function case (v =@ typeRep @(Vector a), u =@ typeRep @(Vector b)) of (Just HRefl, Just HRefl) -> - let + let v' = getVector ndC1 :: Vector a u' = getVector ndC2 :: Vector b in NdArray s' (V.zipWith zipfunc v' u' :: Vector c) @@ -439,7 +444,7 @@ zipArrayWith zipfunc (NdArray s v) (NdArray r u) = -- | Pointwise integer division. Will return an NdArray of type Int. elemDiv :: NdArray -> NdArray -> NdArray -elemDiv (NdArray s v) (NdArray r u) = if s == r then +elemDiv (NdArray s v) (NdArray r u) = if s == r then case v =@= u of Just HRefl -> elemDivVec s v r u Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemDiv") @@ -459,7 +464,7 @@ elemPow = pointwiseZip DType.pow -- | Pointwise exponentiation which forces precision. -- Takes some NdArray of bases, an array of Double exponents and returns an array of Doubles. elemPower :: NdArray -> NdArray -> NdArray -elemPower (NdArray s v) (NdArray r u) = if s == r then +elemPower (NdArray s v) (NdArray r u) = if s == r then case u =@ typeRep @(Vector Double) of Just HRefl -> elemPowerVec s v r u Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemPower") @@ -470,26 +475,26 @@ elemPowerVec s v r u = zipArrayWith (DType.power :: a -> Double -> Double) (NdAr -- * Many Arguments --- | Takes the pointwise sum over all the given NdArrays. If they are different shapes, +-- | Takes the pointwise sum over all the given NdArrays. If they are different shapes, -- the smaller dimensions are padded out with the identity element. -- The sum of the empty list is the singleton 0. sum :: [NdArray] -> NdArray sum [] = singleton (0::Int) sum [nd] = nd -sum ((NdArray s v) : nds) = foldr (\x acc -> padShape sh x + acc) (zeros (vecType v) sh) ((NdArray s v) : nds) +sum (NdArray s v : nds) = foldr (\x acc -> padShape sh x + acc) (zeros (vecType v) sh) (NdArray s v : nds) where sh = maximiseShape (map shape nds) -- Takes the maximum of each element pointwise matching from the end. maximiseShape :: [[Integer]] -> [Integer] maximiseShape [] = [] maximiseShape [sh] = sh -maximiseShape (sh : shs) = +maximiseShape (sh : shs) = let m = maximiseShape shs diff = length sh - length m - in - if diff > 0 - then zipWith max sh (take diff sh ++ m) + in + if diff > 0 + then zipWith max sh (take diff sh ++ m) else zipWith max (take (-diff) m ++ sh) m -- | Finds the mean pointwise over the list of arrays. Smaller arrays are padded out with @@ -497,7 +502,7 @@ maximiseShape (sh : shs) = mean :: [NdArray] -> NdArray mean [] = NdArray [] $ V.fromList ([] :: [Int]) mean nds = s `elemDivide` NdArray sh (V.replicate (size sh) (length nds)) - where + where s = Numskull.sum nds sh = shape s @@ -505,8 +510,8 @@ mean nds = s `elemDivide` NdArray sh (V.replicate (size sh) (length nds)) -------------------------------------------------------------------------------- {- | Converting between the standard dtypes and changing the shapes of arrays. -NB the difference between 'size' and 'shape'. The shape is an Integer list -describing the width of each dimension. Size refers to the total number of +NB the difference between 'size' and 'shape'. The shape is an Integer list +describing the width of each dimension. Size refers to the total number of elements in the array, i.e. the product of the shape. -} @@ -528,9 +533,9 @@ convertDTFromTo _ _ (NdArray s v) = case v =@= (undefined :: Vector a) of matchDType :: NdArray -> NdArray -> NdArray matchDType (NdArray _ v) = convertDTypeTo (vecType v) -{- Helper which checks that the array isn't larger than the shape contraints. +{- Helper which checks that the array isn't larger than the shape contraints. If it is valid the Boolean in the pair will be true and the vector is returned. -If it is invalid the vector is truncated first. +If it is invalid the vector is truncated first. -} constrainSize :: DType a => Integer -> Vector a -> (Bool, Vector a) constrainSize s v = @@ -549,7 +554,7 @@ setSize :: DType a => Integer -> Vector a -> Vector a setSize s v = let (unchanged, u) = constrainSize s v in if unchanged then padSize s u else u -{- | Truncate or pad the NdArray to match the new given size. +{- | Truncate or pad the NdArray to match the new given size. The shape will be collapsed to 1xn. -} -- >>> x = fromList [2,2] [1,2,3,4 :: Int] @@ -560,15 +565,15 @@ The shape will be collapsed to 1xn. resize :: Integer -> NdArray -> NdArray resize s (NdArray _ v) = NdArray [s] (setSize s v) --- | Shape-shift one array to another of the same size (Nothing otherwise). +-- | Shape-shift one array to another of the same size (Nothing otherwise). -- >>> x = fromList [2,3] [1,2,3,4,5,6 :: Int] -- >>> printArray x --- 1 2 --- 3 4 +-- 1 2 +-- 3 4 -- 5 6 -- >>> printArray $ fromJust $ reshape [3,2] x --- 1 2 3 --- 4 5 6 +-- 1 2 3 +-- 4 5 6 reshape :: [Integer] -> NdArray -> Maybe NdArray reshape r (NdArray s v) = if product s == product r then Just $ NdArray r v @@ -576,55 +581,55 @@ reshape r (NdArray s v) = if product s == product r -- Checks that the first shape is smaller or equal to the second. smallerShape :: [Integer] -> [Integer] -> Bool -smallerShape s r = (length s <= length r) && (and $ zipWith (<=) s r) +smallerShape s r = (length s <= length r) && and (zipWith (<=) s r) -- | Adds zero-rows to an array. Will error if you map to a smaller shape. -- >>> x = fromList [2,2] [1,2,3,4 :: Int] -- >>> printArray $ padShape [4,3] x --- 1 2 0 0 --- 3 4 0 0 +-- 1 2 0 0 +-- 3 4 0 0 -- 0 0 0 0 padShape :: [Integer] -> NdArray -> NdArray -padShape r (NdArray s v) = +padShape r (NdArray s v) = let nullVec = V.replicate (size r) (identityElem v) newIndices = V.imap (\i _ -> fromIntegral $ map1DIndex s r (toInteger i) :: Int) v in - if smallerShape s r + if smallerShape s r then NdArray r (V.unsafeUpdate_ nullVec newIndices v) else error "Cannot map to a smaller shape." -- | Truncates the array to be no larger than the specified dimensions. constrainShape :: [Integer] -> NdArray -> NdArray constrainShape r (NdArray s v) = - let + let s' = zipWith min r s sPad = s' ++ replicate (length s - length r) 1 in NdArray s' $ V.ifilter (\i _ -> and $ zipWith (<) (expandInd s (toInteger i)) sPad) v -- | Takes a pair of NdArrays and attempts to copy slices so that they are size matched. --- Arrays are broadcastable if they either match in corresponding dimensions or one is +-- Arrays are broadcastable if they either match in corresponding dimensions or one is -- of dimension size 1 e.g. [2,5,1] and [2,1,6]. Missing dimensions are padded with 1s --- e.g. [1,2,3] and [3] are broadcastable. +-- e.g. [1,2,3] and [3] are broadcastable. broadcast :: (NdArray, NdArray) -> Maybe (NdArray, NdArray) broadcast (NdArray s v, NdArray r u) = let (s',v',r',u') = broadcastDimensions s v r u - newshape = zipWithM (\x y -> if x == y || x == 1 || y == 1 + newshape = zipWithM (\x y -> if x == y || x == 1 || y == 1 then Just (max x y) else Nothing) s' r' in case newshape of Nothing -> Nothing Just ns -> Just ( - NdArray ns $ padRepeats ns m s' v', + NdArray ns $ padRepeats ns m s' v', NdArray ns $ padRepeats ns m r' u') where m = fst $ mapIndicies ns -- Pads out dimensions for broadcasting if one array is dimensionally smaller than another. -- e.g. [1,2,3] and [3]. -broadcastDimensions :: (DType a, DType b) => - [Integer] -> Vector a -> [Integer] -> Vector b -> +broadcastDimensions :: (DType a, DType b) => + [Integer] -> Vector a -> [Integer] -> Vector b -> ([Integer], Vector a, [Integer], Vector b) broadcastDimensions s v r u | sl == rl = (s,v, @@ -643,8 +648,8 @@ broadcastDimensions s v r u rdiff = take diff r -- Pads out a newshape with repetitions of the existing values --- Takes the newshape, its map, the old shape and the vector. -padRepeats :: DType a => +-- Takes the newshape, its map, the old shape and the vector. +padRepeats :: DType a => [Integer] -> M.Map Int [Integer] -> [Integer] -> Vector a -> Vector a padRepeats newshape oneDmap s v = let (_, multiMap) = mapIndicies s @@ -653,27 +658,27 @@ padRepeats newshape oneDmap s v = multiI = oneDmap M.! i -- equivalent multi-index multiWrap = zipWith mod multiI s -- wrap the index over dimensions of size 1 flatWrap = multiMap M.! multiWrap -- collapse the index over the vector - in v V.! flatWrap) + in v V.! flatWrap) -- | Concatenate a list of tensors into a single tensor. All input tensors must have the --- same shape, except for the dimension size of the axis to concatenate on. --- Returns Nothing if the arrays are not all of the same type or matching shapes. +-- same shape, except for the dimension size of the axis to concatenate on. +-- Returns Nothing if the arrays are not all of the same type or matching shapes. concatAlong :: Int -> [NdArray] -> Maybe NdArray concatAlong _ [] = Nothing concatAlong _ [nd] = Just nd concatAlong axis ((NdArray s v):nds) = - case extractVectors (NdArray s v : nds) (vecType v) of + case extractVectors (NdArray s v : nds) (vecType v) of Nothing -> Nothing - Just vs -> - case concatAlongVec vs (map shape (NdArray s v : nds)) axis of + Just vs -> + case concatAlongVec vs (map shape (NdArray s v : nds)) axis of Nothing -> Nothing Just (ns, c) -> Just $ NdArray ns c -- Helper for concatenation of vectors and their associated shapes. concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) -concatAlongVec vs shs axis = +concatAlongVec vs shs axis = if not (checkShapeLengths shs) || not (checkAxis axis shs) then Nothing - else + else let -- Calculates the newshape by adding up all the dimensions along the axis axDim = axisDimensions axis shs @@ -683,10 +688,10 @@ concatAlongVec vs shs axis = arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim (newMultiInds, _) = mapIndicies newshape subArrayMaps = map (snd . mapIndicies) shs - in + in Just (newshape, V.generate (length newMultiInds) (\i -> - let + let -- Generating the new vector by converting the new flat index to a multi-index -- then mapping it to a sub-array and index and reading the value. multiI = newMultiInds M.! i @@ -694,9 +699,9 @@ concatAlongVec vs shs axis = array = vs !! arrayNo arrayMap = subArrayMaps !! arrayNo arrayMultiI = replaceNth axis arrayAxInd multiI - in + in vecInd arrayMap array arrayMultiI <-@ typeRep @a - ) + ) ) -- Swaps in a value at the given index @@ -706,27 +711,28 @@ replaceNth n x l = take n l ++ [x] ++ drop (n+1) l -- Checks for the same number of dimensions checkShapeLengths :: [[Integer]] -> Bool checkShapeLengths [] = False -checkShapeLengths shapes = (filter (\sh -> length sh /= baseLen) shapes) == [] +checkShapeLengths shapes = all (\sh -> length sh == baseLen) shapes where baseLen = length $ head shapes -- Checks that each dimension is the same save perhaps the axis one checkAxis :: Int -> [[Integer]] -> Bool checkAxis _ [] = False -checkAxis axis shapes = - let +checkAxis axis shapes = + let dropAxis = map (\sh -> take axis sh ++ drop (axis+1) sh) shapes base = head dropAxis - in 0 <= axis && axis <= length base && (foldr intersect base dropAxis) == base + in 0 <= axis && axis <= length base && + foldr intersect base dropAxis == base -- Gets the size of the dimension of the axis over all the shapes axisDimensions :: Int -> [[Integer]] -> [Integer] -axisDimensions axis shapes = map (!! axis) shapes +axisDimensions axis = map (!! axis) -- | Takes an array, set of sub-indicies and axis and repeatedly takes slices --- of the array restricted to that index along the specified axis. +-- of the array restricted to that index along the specified axis. -- The slices are then concatenated into the final array. gather :: NdArray -> [Integer] -> Integer -> NdArray -gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) +gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) where ax = fromIntegral axis sliceLead = replicate ax (0,-1) @@ -736,17 +742,17 @@ gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [( -- * Rows, Columns and Diagonals -{- | Switches the rows at the two given indicies over. +{- | Switches the rows at the two given indicies over. NB: designed for 2x2 matricies so will only make swaps in the 'front' matrix of a tensor. -} swapRows :: Integer -> Integer -> NdArray -> NdArray swapRows r1 r2 (NdArray s v) - | r1 == r2 = (NdArray s v) + | r1 == r2 = NdArray s v | length s < 2 = error "Too few rows to make swaps." | r1 >= numRows || r2 >= numRows = error "Row index exceeds number of rows." - | otherwise = + | otherwise = let - lenRows = fromIntegral @Integer @Int $ s !! (colI+1) + lenRows = fromIntegral @Integer @Int $ s !! (colI+1) rowInd1 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r1,0] rowInds1 = V.iterateN lenRows succ rowInd1 rowInd2 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r2,0] @@ -766,8 +772,7 @@ diagonal (NdArray s v) = NdArray [fromIntegral $ V.length v'] v' -- Helper to take the leading diagonal in the vector form. diagonalVec :: forall a . DType a => [Integer] -> Vector a -> Vector a -diagonalVec s v = - V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) v +diagonalVec s = V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) where rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) @@ -777,28 +782,28 @@ diagonalVec s v = -- | Reverses the order of axes and switches the elements accordingly. transpose :: NdArray -> NdArray transpose (NdArray sh v) = transposePerm dec (NdArray sh v) - where - l = length sh + where + l = length sh dec = [l-1, l-2 .. 0] -- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) transposePerm :: [Int] -> NdArray -> NdArray transposePerm perm (NdArray sh v) = - let + let sh' = permuteList perm sh perm' = invertPermutation perm (_, toV) = mapIndicies sh (fromU, _) = mapIndicies sh' sz = V.length v - in NdArray sh' $ V.generate sz (\i -> - let + in NdArray sh' $ V.generate sz (\i -> + let multU = fromU M.! i - flatV = toV M.! (permuteList perm' multU) + flatV = toV M.! permuteList perm' multU in v V.! flatV) -- Applies a permutation to a list permuteList :: [Int] -> [a] -> [a] -permuteList perm l = if sort perm /= [0 .. length l -1] +permuteList perm l = if sort perm /= [0 .. length l -1] then error "Invalid permutation given." else map (l!!) perm @@ -810,43 +815,43 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -- | Dot product over matricies of the same shape. dot :: DType a => NdArray -> NdArray -> a -dot nd1 nd2 = foldrA (DType.add) (DType.addId) (nd1*nd2) +dot nd1 nd2 = foldrA DType.add DType.addId (nd1*nd2) --- | Standard matrix multiplication following NumPy conventions. +-- | Standard matrix multiplication following NumPy conventions. -- 1D arrays have the extra dimension pre/appended -- 2D arrays are multiplied as expected -- ND-arrays are broadcast to match each other where possible and treated as stacks of nxm/pxq arrays. matMul :: NdArray -> NdArray -> NdArray matMul (NdArray s v) (NdArray r u) = case v =@= u of - Just HRefl -> - case (reverse s, reverse r) of + Just HRefl -> + case (reverse s, reverse r) of -- Standard matrix multiplication ([m, n], [q, p]) | m == p -> NdArray [n,q] (matMulVec s v r u) -- 1D arrays have the extra dimension pre/appended then result collapses back to 1D ([m], [q, p]) | m == p -> NdArray [q] (matMulVec [1,m] v r u) ([m, n], [p]) | m == p -> NdArray [n] (matMulVec s v [p,1] u) - -- ND-arrays are broadcast to match each other where possible and treated as + -- ND-arrays are broadcast to match each other where possible and treated as -- stacks of nxm/pxq arrays. - ((m : n : _), (q : p : _)) | m == p -> + (m : n : _, q : p : _) | m == p -> let (s', v', _r', u') = broadcastDimensions s v r u stackA = vectorChunksOf (fromIntegral @Integer @Int $ m * n) v' stackB = vectorChunksOf (fromIntegral @Integer @Int $ q * p) u' - stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,q]) stackB + stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,q]) stackB in NdArray (take (length s' -2) s' ++ [n,q]) $ V.concat stackAB - _ -> throw (ShapeMismatch (NdArray s v) (NdArray r u) "matMul") + _ -> throw (ShapeMismatch (NdArray s v) (NdArray r u) "matMul") _ -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "matMul") -- Splits a vector into a list of vectors of the given size. vectorChunksOf :: V.Storable a => Int -> Vector a -> [Vector a] vectorChunksOf _ v | V.null v = [] -vectorChunksOf n v = first : (vectorChunksOf n rest) +vectorChunksOf n v = first : vectorChunksOf n rest where (first, rest) = V.splitAt n v -- Returning the vector result of the standard nxm matMul -matMulVec :: forall a . DType a => +matMulVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector a -> Vector a matMulVec s v r u = let @@ -858,23 +863,101 @@ matMulVec s v r u = in V.generate sz (matMulElem map1 map2 ks . (M.!) oneDkey) --- Calculates the element at position [i,j] in the resultant nxp matrix of a matMul +-- Calculates the element at position [i,j] in the resultant nxp matrix of a matMul matMulElem :: forall a . DType a => ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a matMulElem map1 map2 ks (i:j:_) = foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks matMulElem _ _ _ _ = DType.multId :: a +{- | General matrix multiplication. Calculates alpha*AB + beta*C with the option +to transpose A and B first. +Takes A, B, C, A transpose?, B transpose?, alpha, beta +Returns nothing if the matrix types/sizes do not match. +Will attempt to broadcast the shape of C and convert the types of alpha & beta. + +For more information see: +https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms#Level_3 +NB: if the matricies are integers the scalars will also become integers so you should convert the matricies first +-} +gemm :: (DType a, DType b) => + NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray +gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = + let + -- Apply transposition to A and B if specified + (sAT, vAT) = applyTransposition (sA, vA) transA + (sBT, vBT) = applyTransposition (sB, vB) transB + in + -- Check all the types match + case gemmTyping vAT vBT vC alpha beta of + Nothing -> Nothing + Just (vA', vB', vC', alpha', beta') -> + -- Check A and B have shapes (M,K) and (K, N) + if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing + else + let + alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + sAB = shape alphaAB + in + -- Check if C dimension matches or is broadcastable + if (sC!!0 /= 1 && sC!!0 /= sAB!!0) || (sC!!1 /= 1 && sC!!1 /= sAB!!1) then Nothing + else + let betaC = scale beta' $ if (sC!!0 /= sAB!!0) || (sC!!1 /= sAB!!1) + then snd $ fromJust $ broadcast (alphaAB, NdArray sC vC') + else NdArray sC vC' + in + -- Finally, combine the two + Just (alphaAB + betaC) + +-- Transpose the shape-vector pair if the boolean is true, otherwise return the original. +applyTransposition :: forall a . DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) +applyTransposition (s, v) b = + let + ndT = Numskull.transpose (NdArray s v) + sT = shape ndT + vT = getVector ndT :: Vector a + in + if b then (sT, vT) else (s, v) + +-- Checking all mats are same type & converting scalars if neccersary +gemmTyping :: forall a b c d e . (DType a, DType b, DType c, DType d, DType e) => + Vector a -> Vector b -> Vector c -> d -> e -> + Maybe (Vector a, Vector a, Vector a, a, a) +gemmTyping vA vB vC alpha beta = + case vA =@= vB of + Just HRefl -> + case vA =@= vC of + Just HRefl -> + -- All matricies match types + let + vA' = vA :: Vector a + vB' = vB :: Vector a + vC' = vC :: Vector a + + -- Convert scalar types + alpha' = + case alpha =@= (undefined :: a) of + Just HRefl -> alpha :: a + _ -> DType.rationalToDtype (DType.dtypeToRational alpha) :: a + beta' = + case beta =@= (undefined :: a) of + Just HRefl -> beta :: a + _ -> DType.rationalToDtype (DType.dtypeToRational beta) :: a + in + Just (vA', vB', vC', alpha', beta') + _ -> Nothing + _ -> Nothing + -- * Determinants and Inverses -- | Converts a nxn matrix to upper triangle form. O(n^3). upperTriangle :: NdArray -> NdArray -upperTriangle (NdArray [] v) = (NdArray [] v) -upperTriangle (NdArray (c:rs) v) = +upperTriangle (NdArray [] v) = NdArray [] v +upperTriangle (NdArray (c:rs) v) = let (_, fromMulti) = mapIndicies (c:rs) traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] - in + in NdArray (c:rs) $ triangulateVec fromMulti v traversals (identityElem v) -- Upper triangle form on the hidden vector. @@ -886,7 +969,7 @@ triangulateVec m v ((i,j,k) : trv) r = ratio = if k == 0 then DType.divide (vecInd m v [j,i]) (vecInd m v [i,i]) else r scaled = DType.multiply ratio (vecInd m v [i,k]) newVjk = DType.subtract (vecInd m v [j,k]) scaled - in + in triangulateVec m (v V.// [(jk, newVjk)]) trv ratio {- | Finds the determinant(s) of a tensor. Over matricies of more than two dimensions @@ -900,13 +983,13 @@ determinant (NdArray s v) = case s of [_] -> [DType.addId :: a] [_,_] -> [determinant2D (NdArray s v)] _ | V.null v -> [] - _ -> - let + _ -> + let (c,r) = (s!!(length s -2), last s) (twoDim, rest) = V.splitAt (fromIntegral$c*r) v in (determinant2D (NdArray [c,r] twoDim) : determinant (NdArray s rest)) -{- | Calculates the determinant of a 2D matrix using LU decomposition as described in the +{- | Calculates the determinant of a 2D matrix using LU decomposition as described in the below paper. O(n^3). https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/Makalah-Matdis-2016-051.pdf -} @@ -917,15 +1000,15 @@ determinant2D nd = [2,2] -> determinant2x2 nd -- nxn matricies are row-swapped to find an arrangement with no zeros/identity elements -- in the leading diagonal (pivots) then put into upper triangle form - [c,r] | c == r && (not $ zeroRow nd) -> case swapRowsWith0Pivot nd of + [c,r] | c == r && not (zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> let upperTri = upperTriangle (NdArray s v) - upperTriV = getVector upperTri :: Vector a + upperTriV = getVector upperTri :: Vector a pivots = diagonalVec s upperTriV in -- determinant is the product of the pivots in upper triangle form - V.foldr (DType.multiply) (DType.multId :: a) pivots + V.foldr DType.multiply (DType.multId :: a) pivots -- If the matrix is non-square or has a zero-row/column, it is singular. Nothing -> DType.addId [_,_] -> DType.addId @@ -933,26 +1016,26 @@ determinant2D nd = -- 2x2 quick determinant calculation of ad-bc determinant2x2 :: forall a . DType a => NdArray -> a -determinant2x2 (NdArray _ v) = - let +determinant2x2 (NdArray _ v) = + let mulI i1 i2 = DType.multiply (v V.! i1) (v V.! i2) det = mulI 0 3 `DType.subtract` mulI 1 2 in det <-@ (typeRep @a) - + -- | Checks the whole array for the prescence of a zero-row. zeroRow :: NdArray -> Bool -zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v +zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v -- Checks the array in vector form for a zero-row. zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool -zeroRowVec r v = - let - ident = DType.addId :: a +zeroRowVec r v = + let + ident = DType.addId :: a (row, rest) = V.splitAt r v - in - (not $ V.null v) && - ((V.all (==ident) row) || - (zeroRowVec r rest)) + in + not (V.null v) && + (V.all (==ident) row || + zeroRowVec r rest) {- Repeatedly swaps rows until the matrix is found to be singular or there are no pivots which are zero/identity elem. If singular, returns Nothing. @@ -977,90 +1060,13 @@ swapRowsWith0Pivot (NdArray s v) = {- Extracts the indexed column from the front matrix of a tensor given its shape and vector. -} frontColumn :: forall a . DType a => Int -> [Integer] -> Vector a -> Vector a -frontColumn col s v = V.ifilter +frontColumn col s v = V.ifilter (\i _ -> i `mod` rowLen == col && i < rowLen*columns) $ v <-@ (typeRep @(Vector a)) where rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) -{- | General matrix multiplication. Calculates alpha*AB + beta*C with the option -to transpose A and B first. -Takes A, B, C, A transpose?, B transpose?, alpha, beta -Returns nothing if the matrix types/sizes do not match. -Will attempt to broadcast the shape of C and convert the types of alpha & beta. - -For more information see: -https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms#Level_3 -NB: if the matricies are integers the scalars will also become integers so you should convert the matricies first --} -gemm :: (DType a, DType b) => - NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe (NdArray) -gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = - let - -- Apply transposition to A and B if specified - (sAT, vAT) = applyTransposition (sA, vA) transA - (sBT, vBT) = applyTransposition (sB, vB) transB - in - -- Check all the types match - case gemmTyping vAT vBT vC alpha beta of - Nothing -> Nothing - Just (vA', vB', vC', alpha', beta') -> - -- Check A and B have shapes (M,K) and (K, N) - if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing - else - let - alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) - sAB = shape alphaAB - in - -- Check if C dimension matches or is broadcastable - if (sC!!0 /= 1 && sC!!0 /= sAB!!0) || (sC!!1 /= 1 && sC!!1 /= sAB!!1) then Nothing - else - let betaC = scale beta' $ if (sC!!0 /= sAB!!0) || (sC!!1 /= sAB!!1) - then snd $ fromJust $ broadcast (alphaAB, NdArray sC vC') - else (NdArray sC vC') - in - -- Finally, combine the two - Just $ (alphaAB + betaC) - --- Transpose the shape-vector pair if the boolean is true, otherwise return the original. -applyTransposition :: forall a . DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) -applyTransposition (s, v) b = - let - ndT = Numskull.transpose (NdArray s v) - sT = shape ndT - vT = (getVector ndT) :: Vector a - in - if b then (sT, vT) else (s, v) - --- Checking all mats are same type & converting scalars if neccersary -gemmTyping :: forall a b c d e . (DType a, DType b, DType c, DType d, DType e) => - Vector a -> Vector b -> Vector c -> d -> e -> - Maybe (Vector a, Vector a, Vector a, a, a) -gemmTyping vA vB vC alpha beta = - case vA =@= vB of - Just HRefl -> - case vA =@= vC of - Just HRefl -> - -- All matricies match types - let - vA' = vA :: Vector a - vB' = vB :: Vector a - vC' = vC :: Vector a - - -- Convert scalar types - alpha' = - case alpha =@= (undefined :: a) of - Just HRefl -> alpha :: a - _ -> DType.rationalToDtype (DType.dtypeToRational alpha) :: a - beta' = - case beta =@= (undefined :: a) of - Just HRefl -> beta :: a - _ -> DType.rationalToDtype (DType.dtypeToRational beta) :: a - in - Just (vA', vB', vC', alpha', beta') - _ -> Nothing - _ -> Nothing @@ -1071,4 +1077,4 @@ ndt2 = fromList [2,3] [0,2,4,6,8,10::Int] nd3 = fromList [2,2] [1,2,3,4 :: Int] -nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] \ No newline at end of file +nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] diff --git a/src/Serialisation.hs b/src/Serialisation.hs index a67096b..c5be577 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -1,42 +1,42 @@ {-# LANGUAGE GADTs #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -{-# LANGUAGE RankNTypes #-} - module Serialisation where import DType import NdArray -import Data.Int -import System.IO -import Data.List as List -import Data.List.Split +import Data.Int +import Data.List as List +import Data.List.Split +import qualified Data.Map as M import qualified Data.Vector.Storable as V -import qualified Data.Map as M -import Type.Reflection -import Foreign (Ptr, alloca, mallocBytes) -import Foreign.Storable (poke, peek, sizeOf) -import Data.Word (Word16) +import Data.Word (Word16) +import Foreign (Ptr, alloca, mallocBytes) +import Foreign.Storable (peek, poke, sizeOf) +import System.IO +import Type.Reflection --- | HASKELL TO PYTHON | -- +-- * HASKELL TO PYTHON -- | Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String getNumpyDType (NdArray _ v) = case show $ typeOf v of "Vector Int" -> " " " " " " " " error "Non-standard types cannot be serialised. Yet." - --- | Converts shape list to a string of the Numpy tuple form e.g. (3,2,) + _ -> error "Non-standard types cannot be serialised." + +-- | Converts shape list to a string of the Numpy tuple form e.g. (3,2,) getNumpyShape :: NdArray -> String -getNumpyShape (NdArray s _) = "(" <> (drop 1 $ take (length lshape -1) $ lshape) <> ",)" +getNumpyShape (NdArray s _) = "(" <> drop 1 (take (length lshape -1) lshape) <> ",)" where lshape = show s -- | Gets the maximum memory required for any single element in an array @@ -50,13 +50,13 @@ saveNpy :: FilePath -> NdArray -> IO () saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do let -- Unpacked specs - nd = (NdArray s v) + nd = NdArray s v dtype = getNumpyDType nd shape = getNumpyShape nd vectorSize = (fromInteger $ product s) :: Int elemSize = getElemSize nd -- Header string without length - header = + header = "{'descr': '"<> dtype <> "', " <> "'fortran_order': False, "<> "'shape': "<> shape <> " }" @@ -75,63 +75,48 @@ saveNpy path (NdArray s v) = withBinaryFile path WriteMode $ \h -> do V.unsafeWith v (\ptr -> hPutBuf h ptr (vectorSize * elemSize)) --- | PYTHON TO HASKELL | -- +-- * PYTHON TO HASKELL --- METADATA +-- Splits the metadata into a list of keys and values listDict :: String -> [String] -listDict x = splitOn " " $ (splitOneOf "{}" (filter (/='\'') x)) !! 1 +listDict x = splitOn " " $ splitOneOf "{}" (filter (/='\'') x) !! 1 +-- Pairs adjacent keys and values in the metadata pairDict :: [String] -> [(String, String)] pairDict [] = [] -pairDict (_:[]) = [] +pairDict [_] = [] pairDict (k:v:ls) = (k, v) : pairDict ls ---pyToTypeRep " String -> TypeRep a - -{- -pyToTypeRep :: forall a . String -> (DType a => TypeRep a) -pyToTypeRep dtype = case dtype of - " typeRep @Int :: TypeRep a - --" - " typeRep @Float - --" - --" - --" - _ -> error "Unsupported dtype." --} - --- PAYLOAD -- Read in an element from the handle buffElement :: forall a . DType a => Handle -> IO a buffElement h = do let elemSize = sizeOf (undefined :: a) ptr <- mallocBytes elemSize _ <- hGetBuf h ptr elemSize - val <- peek ptr - pure val + peek ptr -- Read in the complete array as a list from the handle buffArray :: forall a . DType a => TypeRep a -> Handle -> Integer -> [IO a] buffArray _ _ 0 = [] buffArray t h i = do - let buffed = (buffElement h) : buffArray t h (i-1) + let buffed = buffElement h : buffArray t h (i-1) case eqTypeRep (typeOf buffed) (typeRep @[IO a]) of Just HRefl -> buffed _ -> error "Given TypeRep does not match data type." +-- Reads a buffer into an NdArray given the handle, shape and dtype loadPayload :: forall a . DType a => Handle -> [Integer] -> TypeRep a -> IO NdArray loadPayload h sh _ = do - l <- traverse id $ buffArray (typeRep @a) h (product sh) + l <- sequenceA (buffArray (typeRep @a) h (product sh)) pure $ NdArray sh (V.fromList l) --- todo check unicode UTF +-- Todo: check unicode UTF +-- Facilitates conversion from a numpy dtype signature to a typeRep reifyDType :: String -> (forall a . DType a => TypeRep a -> r) -> r reifyDType dtype cont = - case dtype of + case dtype of " cont (typeRep @Int64) - --" cont (typeRep @Int) + " cont (typeRep @Int) " cont (typeRep @Int32) " cont (typeRep @Float) " cont (typeRep @Double) @@ -139,14 +124,13 @@ reifyDType dtype cont = " cont (typeRep @Bool) _ -> error "Unsupported dtype." - -- | Loads an NdArray from a .npy file loadNpy :: FilePath -> IO NdArray loadNpy path = withBinaryFile path ReadMode $ \h -> do -- Unpacks and parses the header to get the array type and size descr <- hGetLine h let - -- Places the dtype description, fortran order and shape in a map + -- Places the dtype description, fortran order and shape in a map metadata = (M.fromList . pairDict . listDict) descr -- Extracts the dtype description e.g. do shapeStrs = filter (/= "") $ splitOn "," $ filter (`notElem`"()") (metadata M.! "shape:") sh = map (read @Integer) shapeStrs -- Calculates the total number of elements in the array - --sz = product sh -- Reads the array itself into a list reifyDType dtype (loadPayload h sh) - --- Try it! It will probably break easily - --- withTempFile -testsave :: IO () -testsave = do saveNpy "./src/testout/test123.npy" (NdArray [3] (V.fromList [1,2,3 :: Float]) ) - -testload :: IO () -testload = do - nd <- loadNpy "./src/testout/test123.npy" - putStrLn $ show $ nd - -{- --- Reads the array itself into a list - l <- traverse id $ case dtype of - " buffInts h sz - --" - --" - " buffFloats h sz - --" - --" - --" - _ -> error "Unsupported dtype." - -- Converts the list & shape to an NdArray - pure $ NdArray sh (V.fromList l) --} \ No newline at end of file diff --git a/src/Typing.hs b/src/Typing.hs index 66df69b..9c62ed0 100644 --- a/src/Typing.hs +++ b/src/Typing.hs @@ -18,10 +18,11 @@ ty = typeOf (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) (=@=) v u = eqTypeRep (ty v) (ty u) +-- | eqTypeRep-like for checking a value against a typeRep. (=@) :: Typeable a => a -> TypeRep b -> Maybe (a :~~: b) -(=@) x t = eqTypeRep (ty x) t +(=@) x = eqTypeRep (ty x) --- Helper asserting a type +-- Helper asserting a type. (<-@) ::Typeable a => a -> TypeRep b -> b (<-@) val t = case eqTypeRep t (ty val) of Just HRefl -> val From 3f2f13ebc372d0aeff7a5b54fcef8bf710420d30 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 10 Aug 2023 12:13:43 +0100 Subject: [PATCH 70/90] adding exception to cabal --- numskull.cabal | 2 +- src/DType.hs | 6 +++--- src/Indexing.hs | 4 +++- src/MatrixForm.hs | 4 ++-- src/Numskull.hs | 19 ++++--------------- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/numskull.cabal b/numskull.cabal index b757437..4ee076c 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -14,7 +14,7 @@ build-type: Simple library import library-deps - exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing, NdArray + exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing, NdArray, NdArrayException -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 diff --git a/src/DType.hs b/src/DType.hs index 17ed766..848c8ba 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -221,9 +221,9 @@ instance DType Bool where -- | Logical reverse implication pow x y = not y || x -- | Logical implication - log _x _y = not x || y - -- | Logical XOR - mod x y = (x || y) && not (x && y) + log x y = not x || y + -- | Logical XOR, but Int result + mod x y = fromEnum $ (x || y) && not (x && y) abs _ = True signum = id ceil = id diff --git a/src/Indexing.hs b/src/Indexing.hs index e512832..4023d30 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -4,6 +4,7 @@ module Indexing where +import Control.Exception import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import qualified Data.Map as M @@ -13,6 +14,7 @@ import NdArray import Typing import qualified DType import DType (DType) +import NdArrayException {- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the @@ -153,7 +155,7 @@ sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ -- Takes a slice of an NdArray at a particular dimension. sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray sliceDim (x,y) d m (NdArray sh v) = - if d >= length sh then throw (ExceededShape d sh) + if d >= length sh then throw (ExceededShape (fromIntegral d) sh) else NdArray (if y' < x' then [] else shrinkNth d (y'-x'+1) sh) (V.ifilter diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index b673db6..392926d 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -63,7 +63,7 @@ padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " -- Separates columns and higher dimensions by newlines. addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] -addNewlines = foldr (\l -> +addNewlines ls xs = foldr (\l -> map (\(i, x) -> if i /= 0 && i `mod` l == 0 then (i, "\n" ++ x) - else (i, x))) \ No newline at end of file + else (i, x))) xs ls \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index d2ee02a..cf32fc3 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -119,15 +119,17 @@ module Numskull ( ) where -import DType (DType) import qualified DType +import DType (DType) import Indexing import MatrixForm import NdArray import NdArrayException +import Serialisation import Typing import Control.Exception +import Control.Monad (zipWithM) import Data.List (elemIndex, intersect, sort, zipWith4) import qualified Data.Map as M import Data.Maybe (fromJust) @@ -135,8 +137,6 @@ import Data.Vector.Storable (Vector) import qualified Data.Vector.Storable as V import Type.Reflection -import Debug.Trace - -- $setup -- >>> import Numskull as N -- >>> import qualified Vector @@ -330,7 +330,7 @@ mapTransform f (NdArray s v) = NdArray s (V.map f v) -- | Multiplies all elements by a scalar. scale :: forall a . DType a => a -> NdArray -> NdArray -scale = mapA DType.multiply +scale x = mapA (DType.multiply x) -- | Takes the absolute value of all elements. absA :: NdArray -> NdArray @@ -1067,14 +1067,3 @@ frontColumn col s v = V.ifilter rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) - - - -ndt1 :: NdArray -ndt1 = fromList [3,2] [1,2,3,4,5,6::Int] -ndt2 :: NdArray -ndt2 = fromList [2,3] [0,2,4,6,8,10::Int] - -nd3 = fromList [2,2] [1,2,3,4 :: Int] - -nd4 = fromList [3,3] [2,5,1, 9,2,7, 4,16,3 ::Float] From 58de203e57c2be504a06bd5c943997d4080112b9 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 09:20:45 +0100 Subject: [PATCH 71/90] small fixes --- .gitignore | 3 ++- numskull.cabal | 7 +++---- src/Indexing.hs | 16 ++++++++-------- src/MatrixForm.hs | 2 +- src/Numskull.hs | 38 +++++++++++++++++++------------------- src/Serialisation.hs | 19 +++++++++++++++++-- 6 files changed, 50 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index daa9e81..67855a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -dist-newstyle/ \ No newline at end of file +dist-newstyle/ +result/ \ No newline at end of file diff --git a/numskull.cabal b/numskull.cabal index 4ee076c..b9e6282 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -35,10 +35,9 @@ test-suite test , hspec , QuickCheck , numskull - --ghc-options: -Wall - -- -Wno-unused-top-binds - ghc-options: -Wno-unused-top-binds - default-language: Haskell2010 + + ghc-options: -Wall + default-language: Haskell2010 test-suite doctest import library-deps diff --git a/src/Indexing.hs b/src/Indexing.hs index 4023d30..dfa7d82 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -30,7 +30,7 @@ shape of the array, [sx,sy,sz,...], as follows: -- * INDEXING --- | Generates the list of all multi-dimensional indicies for a given shape +-- | Generates the list of all multi-dimensional indices for a given shape generateIndicies :: [Integer] -> [[Integer]] generateIndicies = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] @@ -38,8 +38,8 @@ generateIndicies = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] underlying vector and the multi-dimensional index of the NdArray and back, given the NdArray shape. -} -mapIndicies :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) -mapIndicies sh = (M.fromList oneDkey, M.fromList twoDkey) +mapIndices :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) +mapIndices sh = (M.fromList oneDkey, M.fromList twoDkey) where twoDinds = generateIndicies sh oneDkey = zip [0..] twoDinds @@ -51,7 +51,7 @@ vecInd mapp v i = v V.! (mapp M.! i) -- | Converts a shape and multi-index to a 1D index. collapseInd :: [Integer] -> [Integer] -> Integer -collapseInd sh indicies = collapseRun (reverse sh) (reverse indicies) 1 +collapseInd sh indices = collapseRun (reverse sh) (reverse indices) 1 -- Helper for collapseInd collapseRun :: [Integer] -> [Integer] -> Integer -> Integer @@ -102,7 +102,7 @@ value for the array e.g. 0. To avoid this use !?. (!?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a (NdArray s v) !? i = let - -- Converts any negative indicies to their equivalent positives + -- Converts any negative indices to their equivalent positives positives = zipWith positiveInd s i flatInd = fromIntegral $ collapseInd s positives :: Int in @@ -115,7 +115,7 @@ value for the array e.g. 0. To avoid this use !?. -- * SLICING --- | Type which allows you to provide only a single index or a range of indicies. +-- | Type which allows you to provide only a single index or a range of indices. data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) -- | Integrated indexing and slicing. For each dimension you can provide either a single value @@ -124,12 +124,12 @@ data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) (#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) where (m,_) = mapIndicies sh --- Converts an IndexRange to a range of indicies in the standard pair form. +-- Converts an IndexRange to a range of indices in the standard pair form. forceRange :: IndexRange -> (Integer, Integer) forceRange (I i) = (i,i) forceRange (R s t) = (s,t) --- Converts negative indicies to their positive equivalents, counting back +-- Converts negative indices to their positive equivalents, counting back -- from the end of the array (i.e. -1 is the last element). positiveInd :: (Ord a, Num a) => a -> a -> a positiveInd s i = if i < 0 then s+i else i diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index 392926d..ea5c182 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -46,7 +46,7 @@ printArray nd = putStr $ prettyShowArray nd -- | Converts an NdArray to its pretty representation. -- Values along a row are separated whitespace. Along a column, newlines. --- For higher dimensions, an additional newline is added to separate the nxm matricies. +-- For higher dimensions, an additional newline is added to separate the nxm matrices. prettyShowArray :: NdArray -> String prettyShowArray (NdArray s v) = conc <> "\n" where diff --git a/src/Numskull.hs b/src/Numskull.hs index cf32fc3..6823065 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -624,7 +624,7 @@ broadcast (NdArray s v, NdArray r u) = Just ns -> Just ( NdArray ns $ padRepeats ns m s' v', NdArray ns $ padRepeats ns m r' u') - where m = fst $ mapIndicies ns + where m = fst $ mapIndices ns -- Pads out dimensions for broadcasting if one array is dimensionally smaller than another. -- e.g. [1,2,3] and [3]. @@ -652,7 +652,7 @@ broadcastDimensions s v r u padRepeats :: DType a => [Integer] -> M.Map Int [Integer] -> [Integer] -> Vector a -> Vector a padRepeats newshape oneDmap s v = - let (_, multiMap) = mapIndicies s + let (_, multiMap) = mapIndices s in V.generate (fromIntegral $ product newshape) (\i -> let multiI = oneDmap M.! i -- equivalent multi-index @@ -686,8 +686,8 @@ concatAlongVec vs shs axis = -- Each array to be concatenated is given a number to index it with -- Values are indexed by array number, then by position in the array arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim - (newMultiInds, _) = mapIndicies newshape - subArrayMaps = map (snd . mapIndicies) shs + (newMultiInds, _) = mapIndices newshape + subArrayMaps = map (snd . mapIndices) shs in Just (newshape, V.generate (length newMultiInds) (\i -> @@ -728,7 +728,7 @@ checkAxis axis shapes = axisDimensions :: Int -> [[Integer]] -> [Integer] axisDimensions axis = map (!! axis) --- | Takes an array, set of sub-indicies and axis and repeatedly takes slices +-- | Takes an array, set of sub-indices and axis and repeatedly takes slices -- of the array restricted to that index along the specified axis. -- The slices are then concatenated into the final array. gather :: NdArray -> [Integer] -> Integer -> NdArray @@ -742,8 +742,8 @@ gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [( -- * Rows, Columns and Diagonals -{- | Switches the rows at the two given indicies over. -NB: designed for 2x2 matricies so will only make swaps in the 'front' matrix of a tensor. +{- | Switches the rows at the two given indices over. +NB: designed for 2x2 matrices so will only make swaps in the 'front' matrix of a tensor. -} swapRows :: Integer -> Integer -> NdArray -> NdArray swapRows r1 r2 (NdArray s v) @@ -792,8 +792,8 @@ transposePerm perm (NdArray sh v) = let sh' = permuteList perm sh perm' = invertPermutation perm - (_, toV) = mapIndicies sh - (fromU, _) = mapIndicies sh' + (_, toV) = mapIndices sh + (fromU, _) = mapIndices sh' sz = V.length v in NdArray sh' $ V.generate sz (\i -> let @@ -813,7 +813,7 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -- * Multiplication --- | Dot product over matricies of the same shape. +-- | Dot product over matrices of the same shape. dot :: DType a => NdArray -> NdArray -> a dot nd1 nd2 = foldrA DType.add DType.addId (nd1*nd2) @@ -855,10 +855,10 @@ matMulVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector a -> Vector a matMulVec s v r u = let - oneDkey = fst $ mapIndicies [s!!0, r!!1] + oneDkey = fst $ mapIndices [s!!0, r!!1] sz = M.size oneDkey - map1 = vecInd (snd $ mapIndicies s) v - map2 = vecInd (snd $ mapIndicies r) u + map1 = vecInd (snd $ mapIndices s) v + map2 = vecInd (snd $ mapIndices r) u ks = [0 .. (s!!1 -1)] in V.generate sz (matMulElem map1 map2 ks . (M.!) oneDkey) @@ -878,7 +878,7 @@ Will attempt to broadcast the shape of C and convert the types of alpha & beta. For more information see: https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms#Level_3 -NB: if the matricies are integers the scalars will also become integers so you should convert the matricies first +NB: if the matrices are integers the scalars will also become integers so you should convert the matrices first -} gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray @@ -928,7 +928,7 @@ gemmTyping vA vB vC alpha beta = Just HRefl -> case vA =@= vC of Just HRefl -> - -- All matricies match types + -- All matrices match types let vA' = vA :: Vector a vB' = vB :: Vector a @@ -955,7 +955,7 @@ upperTriangle :: NdArray -> NdArray upperTriangle (NdArray [] v) = NdArray [] v upperTriangle (NdArray (c:rs) v) = let - (_, fromMulti) = mapIndicies (c:rs) + (_, fromMulti) = mapIndices (c:rs) traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] in NdArray (c:rs) $ triangulateVec fromMulti v traversals (identityElem v) @@ -972,7 +972,7 @@ triangulateVec m v ((i,j,k) : trv) r = in triangulateVec m (v V.// [(jk, newVjk)]) trv ratio -{- | Finds the determinant(s) of a tensor. Over matricies of more than two dimensions +{- | Finds the determinant(s) of a tensor. Over matrices of more than two dimensions each 2D matrix's determinant is individually calculated and concatenated together (as in numpy: https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html ). If the matrix is non-square it is assumed to be padded out and will have determinant of 0 @@ -996,9 +996,9 @@ https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/M determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = case shape nd of - -- 2x2 matricies are calculated quickly with the standard ad-bc + -- 2x2 matrices are calculated quickly with the standard ad-bc [2,2] -> determinant2x2 nd - -- nxn matricies are row-swapped to find an arrangement with no zeros/identity elements + -- nxn matrices are row-swapped to find an arrangement with no zeros/identity elements -- in the leading diagonal (pivots) then put into upper triangle form [c,r] | c == r && not (zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> diff --git a/src/Serialisation.hs b/src/Serialisation.hs index c5be577..fc38923 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -7,11 +7,13 @@ module Serialisation where import DType import NdArray +import Typing import Data.Int import Data.List as List import Data.List.Split import qualified Data.Map as M +import Data.Maybe (isJust) import qualified Data.Vector.Storable as V import Data.Word (Word16) import Foreign (Ptr, alloca, mallocBytes) @@ -23,16 +25,30 @@ import Type.Reflection -- | Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String +{- getNumpyDType (NdArray _ v) = case show $ typeOf v of "Vector Int" -> " " " " " " " " error "Non-standard types cannot be serialised." +-} +getNumpyDType (NdArray _ v) + | isType (typeRep @Int) = " Vector a -> TypeRep a + vectorType _ = typeRep @a + isType t = isJust (eqTypeRep (vectorType v) t) -- | Converts shape list to a string of the Numpy tuple form e.g. (3,2,) getNumpyShape :: NdArray -> String @@ -116,7 +132,6 @@ reifyDType :: String -> (forall a . DType a => TypeRep a -> r) -> r reifyDType dtype cont = case dtype of " cont (typeRep @Int64) - " cont (typeRep @Int) " cont (typeRep @Int32) " cont (typeRep @Float) " cont (typeRep @Double) From d72ddfd730ccb383fbd31e20856bdcef39c7a9e4 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 11:02:48 +0100 Subject: [PATCH 72/90] more efficient slice --- src/Indexing.hs | 32 ++++++++++++++++++++++++++++---- src/Serialisation.hs | 3 ++- src/Test.hs | 12 ++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/Test.hs diff --git a/src/Indexing.hs b/src/Indexing.hs index dfa7d82..f4bd51f 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -121,8 +121,9 @@ data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) -- | Integrated indexing and slicing. For each dimension you can provide either a single value -- or a range of values where a slice will be taken. (#!+) :: NdArray -> [IndexRange] -> NdArray -(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) - where (m,_) = mapIndicies sh +(#!+) nd irs = slice (map forceRange irs) nd +--(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) +-- where (m,_) = mapIndices sh -- Converts an IndexRange to a range of indices in the standard pair form. forceRange :: IndexRange -> (Integer, Integer) @@ -137,13 +138,35 @@ positiveInd s i = if i < 0 then s+i else i {- | Takes a series of ranges corresponding to each dimension in the array and returns the sub-array. Indicies are inclusive and can be negative. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray -slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) - where (m,_) = mapIndicies sh +slice sl (NdArray s v) = + let + sl' = zipWith (\(x,y) sk -> (positiveInd sk x, positiveInd sk y)) sl s + inds = sequence $ map (\(x, y) -> [x..y]) sl' + flatinds = V.fromList $ map (fromInteger @Int . collapseInd s) inds + newshape = map (\(x,y) -> y-x+1) sl' + in NdArray newshape $ V.map (v V.!) flatinds +{- + collapse index ober the zio of the fsts and snds + let + ranges = zipWith (\(i,j) t -> [t*i, t*(i+1) .. t*j]) sl (V.toList st) + indicies = V.fromList $ (map sum . sequence) ranges + sh' = V.fromList $ map (\(i,j) -> j-i+1) sl + v' = V.map (v V.!) indicies + in + NdArray sh' (defStride sh') v' +-} + +{- | Takes a series of ranges corresponding to each dimension in the array and returns +the sub-array. Indicies are inclusive and can be negative. -} +--slice :: [(Integer, Integer)] -> NdArray -> NdArray +--slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) +-- where (m,_) = mapIndices sh -- | Equivalent slicing operator. (!/) :: NdArray -> [(Integer, Integer)] -> NdArray (!/) nd ss = slice ss nd +{- -- Takes a slice on an NdArray given the mapping from the vector index to NdArray index. -- Iterates through each dimension of the slice one at a time. sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray @@ -175,3 +198,4 @@ shrinkNth _ _ [] = [] shrinkNth n newVal (x:xs) | n == 0 = if newVal < x then newVal:xs else x:xs | otherwise = x:shrinkNth (n-1) newVal xs +-} \ No newline at end of file diff --git a/src/Serialisation.hs b/src/Serialisation.hs index fc38923..82860c3 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -46,8 +46,9 @@ getNumpyDType (NdArray _ v) | isType (typeRep @Char) = "U1" | otherwise = error "Non-standard types cannot be serialised." where - vectorType :: forall a . DType a => Vector a -> TypeRep a + vectorType :: forall a . DType a => V.Vector a -> TypeRep a vectorType _ = typeRep @a + isType :: DType a => TypeRep a -> Bool isType t = isJust (eqTypeRep (vectorType v) t) -- | Converts shape list to a string of the Numpy tuple form e.g. (3,2,) diff --git a/src/Test.hs b/src/Test.hs new file mode 100644 index 0000000..360d524 --- /dev/null +++ b/src/Test.hs @@ -0,0 +1,12 @@ +module Test where + +{-cartProdN :: [(Int,Int)] -> [[Int]] +cartProdN = foldr + (\(l,u) as -> + [ x : a + | x <- [l..u] + , a <- as ]) + [[]] +-} + +thing xs ys = sequence $ zipWith (\x y -> [x..y]) xs ys \ No newline at end of file From e560f6ab0443dccdebac478ca340fe7d7299aede Mon Sep 17 00:00:00 2001 From: Rowan Date: Wed, 16 Aug 2023 17:31:58 +0100 Subject: [PATCH 73/90] new parser work for pretty slicing parser working trying to get ihaskell to work quasisliceing slicing done & presentation notebook works updating updated docs exclusive indexing negative pretty indices slice negative variables --- cabal.project | 4 - ihaskell/default.nix | 10 + .../Rowan-Presentation-checkpoint.ipynb | 502 ++++++++++++++++++ ihaskell/demo/Rowan-Presentation.ipynb | 502 ++++++++++++++++++ ihaskell/demo/default.nix | 10 + ihaskell/demo/serialisationdemo.npy | Bin 0 -> 200 bytes ihaskell/demo/start.sh | 2 + ihaskell/pkgs.nix | 12 + ihaskell/rise.nix | 10 + ihaskell/updater | 26 + ihaskell/versions.json | 16 + nix/sources.json | 60 --- numskull.cabal | 64 ++- numskull.nix | 4 +- src/Indexing.hs | 65 +-- src/Numskull.hs | 94 ++-- src/QuasiSlice.hs | 145 +++++ src/QuasiSlice/Quote.hs | 58 ++ src/testout/test123.npy | Bin 152 -> 0 bytes test/Test/wiki/Expr.hs | 84 +++ test/Test/wiki/Expr/Quote.hs | 46 ++ 21 files changed, 1551 insertions(+), 163 deletions(-) create mode 100644 ihaskell/default.nix create mode 100644 ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb create mode 100644 ihaskell/demo/Rowan-Presentation.ipynb create mode 100644 ihaskell/demo/default.nix create mode 100644 ihaskell/demo/serialisationdemo.npy create mode 100755 ihaskell/demo/start.sh create mode 100644 ihaskell/pkgs.nix create mode 100644 ihaskell/rise.nix create mode 100644 ihaskell/updater create mode 100644 ihaskell/versions.json create mode 100644 src/QuasiSlice.hs create mode 100644 src/QuasiSlice/Quote.hs delete mode 100644 src/testout/test123.npy create mode 100644 test/Test/wiki/Expr.hs create mode 100644 test/Test/wiki/Expr/Quote.hs diff --git a/cabal.project b/cabal.project index 5ad8ca2..b764c34 100644 --- a/cabal.project +++ b/cabal.project @@ -1,6 +1,2 @@ packages: . -source-repository-package - type: git - location: https://github.com/cchalmers/dense.git - tag: 6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262 \ No newline at end of file diff --git a/ihaskell/default.nix b/ihaskell/default.nix new file mode 100644 index 0000000..469aa13 --- /dev/null +++ b/ihaskell/default.nix @@ -0,0 +1,10 @@ +let + pkgs = import ./pkgs.nix; + nixpkgs = import pkgs.nixpkgs {}; + notebooks = map (folder: { + name = folder; + path = import (./. + "/${folder}"); + }); +in nixpkgs.linkFarm "notebooks" (notebooks [ + "codensity", +]) diff --git a/ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb b/ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb new file mode 100644 index 0000000..25b9c6b --- /dev/null +++ b/ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Numskull Demo!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "{-# LANGUAGE TypeApplications #-}\n", + "{-# LANGUAGE TemplateHaskell #-}\n", + "{-# LANGUAGE QuasiQuotes #-}\n", + "import Numskull\n", + "import Data.Maybe (fromJust)\n", + "import Type.Reflection\n", + "\n", + "p = printArray" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It's easy to make arrays." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0 4.0 6.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3.14" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1 2 3 4 5 \n", + " 6 7 8 9 10 \n", + "11 12 13 14 15 \n", + "16 17 18 19 20" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 0 0 \n", + "0 0 0 \n", + "0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 2 \n", + "3 4 \n", + "5 6" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "p $ fromList [3] [2,4,6]\n", + "\n", + "p $ singleton 3.14\n", + "\n", + "p.fromJust $ reshape [4,5] $ arange 1 (20::Int)\n", + "\n", + "p $ zeros (typeRep @Int) [3,3]\n", + "\n", + "l :: TreeMatrix Int\n", + "l = A [A [B 1, B 2],\n", + " A [B 3, B 4],\n", + " A [B 5, B 6]]\n", + "p $ fromMatrix l" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or take slices of them..." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3D Array:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3 1 4 \n", + "1 5 9 \n", + "2 6 5 \n", + "\n", + "3 5 8 \n", + "9 7 9 \n", + "3 2 3" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced, but fancier:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "piNd = fromList [2,3,3] [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3::Int]\n", + "\n", + "putStrLn \"3D Array:\"\n", + "p piNd\n", + "\n", + "putStrLn \"Sliced:\"\n", + "p $ slice [(0,0), (1,2)] piNd\n", + "\n", + "putStrLn \"Sliced, but fancier:\"\n", + "p $ piNd /! [q|0,1:2|]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And switch values or even types" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100 3 6 10 15 21 28 36 45" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1.0 3.0 6.0 10.0 15.0 21.0 28.0 36.0 45.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 1 0 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "intNd = fromListFlat [1, 3, 6, 10, 15, 21, 28, 36, 45 :: Int]\n", + "boolNd = fromListFlat [True, True, False, True]\n", + "\n", + "p $ update intNd [0] 100\n", + "\n", + "p $ convertDTypeTo (typeRep @Double) intNd\n", + "p $ matchDType intNd boolNd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And do all sorts of fun maths!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Numeracy:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 10 0 \n", + "30 40 0 \n", + " 0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Powers/logs:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 1 4 \n", + " 9 16 25 \n", + "36 49 64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Average:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "5 5 1 \n", + "6 7 2 \n", + "3 3 4" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Transpose & diagonal:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 3 6 \n", + "1 4 7 \n", + "2 5 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 4 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Matrix multiplication:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 2.0 3.0 \n", + " 6.0 11.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "16.0 23.0 \n", + "24.0 37.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Determinant:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[40.0]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nd1 = fromList [3,3] [0..8::Int]\n", + "nd2 = padShape [3,3] $ fromList [2,2] [10,10,10,10::Int]\n", + "\n", + "putStrLn \"Numeracy:\"\n", + "p $ nd1 + nd2\n", + "p $ Numskull.sum [nd1, nd2]\n", + "p $ nd1 * nd2\n", + "\n", + "putStrLn \"Powers/logs:\"\n", + "p $ elemPow nd1 (fromList [3,3] $ replicate 9 (2::Int))\n", + "\n", + "putStrLn \"Average:\"\n", + "p $ mean [nd1, nd2]\n", + "\n", + "putStrLn \"Transpose & diagonal:\"\n", + "p $ transpose nd1\n", + "p $ diagonal nd1\n", + "\n", + "putStrLn \"Matrix multiplication:\"\n", + "nd3 = fromList [2,2] [0..3::Float]\n", + "nd4 = fromList [2,2] [4..7::Float]\n", + "p $ matMul nd3 nd3\n", + "m = fromJust (gemm nd3 nd3 nd4 True False 3 1)\n", + "p m\n", + "\n", + "putStrLn \"Determinant:\"\n", + "print (determinant m :: [Float])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the built-in numskull operations aren't good enough for you, and you don't want to write your own, just use NumPy.\n", + "\n", + "NumSkull will serialise most standard DType arrays to NumPy .npy files and back. But you're just going to have to trust me a bit here..." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1 2 \n", + "3 4 5 \n", + "6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "saveNpy \"./serialisationdemo.npy\" nd1\n", + "loadNpy \"./serialisationdemo.npy\" >>= p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Haskell", + "language": "haskell", + "name": "haskell" + }, + "language_info": { + "codemirror_mode": "ihaskell", + "file_extension": ".hs", + "mimetype": "text/x-haskell", + "name": "haskell", + "pygments_lexer": "Haskell", + "version": "9.0.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ihaskell/demo/Rowan-Presentation.ipynb b/ihaskell/demo/Rowan-Presentation.ipynb new file mode 100644 index 0000000..25b9c6b --- /dev/null +++ b/ihaskell/demo/Rowan-Presentation.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Numskull Demo!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "{-# LANGUAGE TypeApplications #-}\n", + "{-# LANGUAGE TemplateHaskell #-}\n", + "{-# LANGUAGE QuasiQuotes #-}\n", + "import Numskull\n", + "import Data.Maybe (fromJust)\n", + "import Type.Reflection\n", + "\n", + "p = printArray" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It's easy to make arrays." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0 4.0 6.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3.14" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1 2 3 4 5 \n", + " 6 7 8 9 10 \n", + "11 12 13 14 15 \n", + "16 17 18 19 20" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 0 0 \n", + "0 0 0 \n", + "0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 2 \n", + "3 4 \n", + "5 6" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "p $ fromList [3] [2,4,6]\n", + "\n", + "p $ singleton 3.14\n", + "\n", + "p.fromJust $ reshape [4,5] $ arange 1 (20::Int)\n", + "\n", + "p $ zeros (typeRep @Int) [3,3]\n", + "\n", + "l :: TreeMatrix Int\n", + "l = A [A [B 1, B 2],\n", + " A [B 3, B 4],\n", + " A [B 5, B 6]]\n", + "p $ fromMatrix l" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or take slices of them..." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3D Array:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3 1 4 \n", + "1 5 9 \n", + "2 6 5 \n", + "\n", + "3 5 8 \n", + "9 7 9 \n", + "3 2 3" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced, but fancier:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "piNd = fromList [2,3,3] [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3::Int]\n", + "\n", + "putStrLn \"3D Array:\"\n", + "p piNd\n", + "\n", + "putStrLn \"Sliced:\"\n", + "p $ slice [(0,0), (1,2)] piNd\n", + "\n", + "putStrLn \"Sliced, but fancier:\"\n", + "p $ piNd /! [q|0,1:2|]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And switch values or even types" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100 3 6 10 15 21 28 36 45" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1.0 3.0 6.0 10.0 15.0 21.0 28.0 36.0 45.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 1 0 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "intNd = fromListFlat [1, 3, 6, 10, 15, 21, 28, 36, 45 :: Int]\n", + "boolNd = fromListFlat [True, True, False, True]\n", + "\n", + "p $ update intNd [0] 100\n", + "\n", + "p $ convertDTypeTo (typeRep @Double) intNd\n", + "p $ matchDType intNd boolNd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And do all sorts of fun maths!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Numeracy:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 10 0 \n", + "30 40 0 \n", + " 0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Powers/logs:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 1 4 \n", + " 9 16 25 \n", + "36 49 64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Average:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "5 5 1 \n", + "6 7 2 \n", + "3 3 4" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Transpose & diagonal:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 3 6 \n", + "1 4 7 \n", + "2 5 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 4 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Matrix multiplication:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 2.0 3.0 \n", + " 6.0 11.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "16.0 23.0 \n", + "24.0 37.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Determinant:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[40.0]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nd1 = fromList [3,3] [0..8::Int]\n", + "nd2 = padShape [3,3] $ fromList [2,2] [10,10,10,10::Int]\n", + "\n", + "putStrLn \"Numeracy:\"\n", + "p $ nd1 + nd2\n", + "p $ Numskull.sum [nd1, nd2]\n", + "p $ nd1 * nd2\n", + "\n", + "putStrLn \"Powers/logs:\"\n", + "p $ elemPow nd1 (fromList [3,3] $ replicate 9 (2::Int))\n", + "\n", + "putStrLn \"Average:\"\n", + "p $ mean [nd1, nd2]\n", + "\n", + "putStrLn \"Transpose & diagonal:\"\n", + "p $ transpose nd1\n", + "p $ diagonal nd1\n", + "\n", + "putStrLn \"Matrix multiplication:\"\n", + "nd3 = fromList [2,2] [0..3::Float]\n", + "nd4 = fromList [2,2] [4..7::Float]\n", + "p $ matMul nd3 nd3\n", + "m = fromJust (gemm nd3 nd3 nd4 True False 3 1)\n", + "p m\n", + "\n", + "putStrLn \"Determinant:\"\n", + "print (determinant m :: [Float])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the built-in numskull operations aren't good enough for you, and you don't want to write your own, just use NumPy.\n", + "\n", + "NumSkull will serialise most standard DType arrays to NumPy .npy files and back. But you're just going to have to trust me a bit here..." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1 2 \n", + "3 4 5 \n", + "6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "saveNpy \"./serialisationdemo.npy\" nd1\n", + "loadNpy \"./serialisationdemo.npy\" >>= p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Haskell", + "language": "haskell", + "name": "haskell" + }, + "language_info": { + "codemirror_mode": "ihaskell", + "file_extension": ".hs", + "mimetype": "text/x-haskell", + "name": "haskell", + "pygments_lexer": "Haskell", + "version": "9.0.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ihaskell/demo/default.nix b/ihaskell/demo/default.nix new file mode 100644 index 0000000..b4e7327 --- /dev/null +++ b/ihaskell/demo/default.nix @@ -0,0 +1,10 @@ +let + pkgs = import ../pkgs.nix; +in import "${pkgs.ihaskell}/release.nix" rec { + nixpkgs = import pkgs.nixpkgs {}; + compiler = "ghc902"; + packages = self: with self; [ + (import ../../default.nix + { inherit nixpkgs; }) + ]; +} diff --git a/ihaskell/demo/serialisationdemo.npy b/ihaskell/demo/serialisationdemo.npy new file mode 100644 index 0000000000000000000000000000000000000000..9f22838b87d6868cc26aaa0ed297ac417719bed0 GIT binary patch literal 200 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlWC!@qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= uXCxM+0{I%oI>tJh3bhL411<(AV1&|4P?{M^vp{K9D9r|?*`YKClm-A6_8XW0 literal 0 HcmV?d00001 diff --git a/ihaskell/demo/start.sh b/ihaskell/demo/start.sh new file mode 100755 index 0000000..827bb99 --- /dev/null +++ b/ihaskell/demo/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +$(nix-build)/bin/jupyter-notebook diff --git a/ihaskell/pkgs.nix b/ihaskell/pkgs.nix new file mode 100644 index 0000000..56f07ef --- /dev/null +++ b/ihaskell/pkgs.nix @@ -0,0 +1,12 @@ + +let + fetcher = { owner, repo, rev, sha256, ... }: builtins.fetchTarball { + inherit sha256; + url = "https://github.com/${owner}/${repo}/tarball/${rev}"; + }; + nixpkgs = import (fetcher (builtins.fromJSON (builtins.readFile ./versions.json)).nixpkgs) { overlays = [ ]; }; + lib = nixpkgs.lib; + versions = lib.mapAttrs + (_: fetcher) + (builtins.fromJSON (builtins.readFile ./versions.json)); +in versions diff --git a/ihaskell/rise.nix b/ihaskell/rise.nix new file mode 100644 index 0000000..9c9e077 --- /dev/null +++ b/ihaskell/rise.nix @@ -0,0 +1,10 @@ +pythonPackages: pythonPackages.buildPythonPackage rec { + pname = "rise"; + version = "5.6.0"; + name = "${pname}-${version}"; + src = builtins.fetchurl { + url = "https://files.pythonhosted.org/packages/source/r/${pname}/${name}.tar.gz"; + sha256 = "09lfcm2zdi5k11af5c5nx4bnx2vr36z90skw0jp3mri7pqymrr1b"; + }; + propagatedBuildInputs = [ pythonPackages.notebook ]; +} diff --git a/ihaskell/updater b/ihaskell/updater new file mode 100644 index 0000000..5d01cc5 --- /dev/null +++ b/ihaskell/updater @@ -0,0 +1,26 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash +#! nix-shell -p curl jq nix + +set -eufo pipefail + +FILE=$1 +PROJECT=$2 + +OWNER=$(jq -r '.[$project].owner' --arg project "$PROJECT" < "$FILE") +REPO=$(jq -r '.[$project].repo' --arg project "$PROJECT" < "$FILE") +DEFAULT_BRANCH=$(jq -r '.[$project].branch // "master"' --arg project "$PROJECT" < "$FILE") + +BRANCH=${3:-$DEFAULT_BRANCH} + +REV=$(curl "https://api.github.com/repos/$OWNER/$REPO/branches/$BRANCH" | jq -r '.commit.sha') +SHA256=$(nix-prefetch-url --unpack "https://github.com/$OWNER/$REPO/tarball/$REV") +TJQ=$(jq '.[$project] = {owner: $owner, repo: $repo, branch: $branch, rev: $rev, sha256: $sha256}' \ + --arg project "$PROJECT" \ + --arg owner "$OWNER" \ + --arg repo "$REPO" \ + --arg branch "$BRANCH" \ + --arg rev "$REV" \ + --arg sha256 "$SHA256" \ + < "$FILE") +[[ $? == 0 ]] && echo "${TJQ}" >| "$FILE" diff --git a/ihaskell/versions.json b/ihaskell/versions.json new file mode 100644 index 0000000..c568e8d --- /dev/null +++ b/ihaskell/versions.json @@ -0,0 +1,16 @@ +{ + "ihaskell": { + "owner": "IHaskell", + "repo": "IHaskell", + "branch": "master", + "rev": "8afa4e22c5724da89fec85a599ee129ab5b4cb9a", + "sha256": "0rkvqrpnsyp33x8mzh1v48vm96bpmza14nl6ah1sgjfbp86ihi8p" + }, + "nixpkgs": { + "owner": "NixOS", + "repo": "nixpkgs", + "branch": "nixos-22.11", + "rev": "da26ae9f6ce2c9ab380c0f394488892616fc5a6a", + "sha256": "1l3xhsnj0msvrf2qz86j4lmbpisvinf4cf1d89qm73zjh5qigzq4" + } +} diff --git a/nix/sources.json b/nix/sources.json index b2d77fa..8a66ea7 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -1,40 +1,4 @@ { - "clash-compiler": { - "branch": "1.4", - "description": "Haskell to VHDL/Verilog/SystemVerilog compiler", - "homepage": "https://www.clash-lang.org/", - "owner": "clash-lang", - "repo": "clash-compiler", - "rev": "fa01fd98799cd7c00cae44a9df847142410f1618", - "sha256": "0b42gschkb7sk958a7j529lcw9kmzikhcqjxblm43lggwwy24hns", - "type": "tarball", - "url": "https://github.com/clash-lang/clash-compiler/archive/fa01fd98799cd7c00cae44a9df847142410f1618.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "dense": { - "branch": "master", - "description": "dense Haskell library", - "homepage": "", - "owner": "cchalmers", - "repo": "dense", - "rev": "6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262", - "sha256": "1v8nx36f8yhifn8jcv24jjwm2msnhg8lx5wji28ci9vr0swq72ly", - "type": "tarball", - "url": "https://github.com/cchalmers/dense/archive/6eced9f5a3ab6b5026fe4f7ab4f67a8bce4d6262.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "niv": { - "branch": "master", - "description": "Easy dependency management for Nix projects", - "homepage": "https://github.com/nmattia/niv", - "owner": "nmattia", - "repo": "niv", - "rev": "099f9ea92169c2edbc3ca33313b68f5e9686e800", - "sha256": "1rqaw2r5wbbfpbxjgwrwzf619la1iqvzp3mhq1mfz9vk4hjd3335", - "type": "tarball", - "url": "https://github.com/nmattia/niv/archive/099f9ea92169c2edbc3ca33313b68f5e9686e800.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, "nixpkgs": { "branch": "nixos-23.05", "description": "Nix Packages collection", @@ -46,29 +10,5 @@ "type": "tarball", "url": "https://github.com/NixOS/nixpkgs/archive/8163a64662b43848802092d52015ef60777d6129.tar.gz", "url_template": "https://github.com///archive/.tar.gz" - }, - "revealjs": { - "branch": "3.8.0", - "description": "The HTML Presentation Framework", - "homepage": "https://revealjs.com", - "owner": "hakimel", - "repo": "reveal.js", - "rev": "3da09f1fef1fa3c140d2daa5bdee2a32683dd964", - "sha256": "14cva2hxdv4gxpz2a996qs8xhxffw97a90gkz2mmgdczh1kyn1sc", - "type": "tarball", - "url": "https://github.com/hakimel/reveal.js/archive/3da09f1fef1fa3c140d2daa5bdee2a32683dd964.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "yoda": { - "branch": "0.1.3.0", - "description": "A simple combinator library", - "homepage": null, - "owner": "zenzike", - "repo": "yoda", - "rev": "fb3517e6de9187ece52d3fd0485d5564ac2b0ae8", - "sha256": "115qjyi77nq52k2d9777wwbwilcaiq7y79yv08mdnikk9mrhb212", - "type": "tarball", - "url": "https://github.com/zenzike/yoda/archive/fb3517e6de9187ece52d3fd0485d5564ac2b0ae8.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" } } \ No newline at end of file diff --git a/numskull.cabal b/numskull.cabal index b9e6282..1449a04 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -14,39 +14,51 @@ build-type: Simple library import library-deps - exposed-modules: Numskull, Serialisation, DType, MatrixForm, Indexing, Typing, NdArray, NdArrayException + exposed-modules: Numskull + , NdArray + , NdArrayException + , DType + --, MatrixForm + --, Indexing + --, QuasiSlice + --, QuasiSlice.Quote + --, Typing + --, Serialisation -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 , vector , split , containers + , parsec + , template-haskell hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall -test-suite test - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Test.hs - other-modules: Test.Numskull - Test.Serialisation - build-depends: base >=4.13.0.0 - , hspec - , QuickCheck - , numskull - - ghc-options: -Wall - default-language: Haskell2010 - -test-suite doctest - import library-deps - type: exitcode-stdio-1.0 - hs-source-dirs: test, src - main-is: DocTest.hs - build-depends: base >=4.13.0.0 - , doctest - , vector - , split - ghc-options: -Wall - default-language: Haskell2010 \ No newline at end of file +--test-suite test +-- type: exitcode-stdio-1.0 +-- hs-source-dirs: test +-- main-is: Test.hs +-- other-modules: Test.Numskull +-- Test.Serialisation +-- build-depends: base >=4.13.0.0 +-- , hspec +-- , QuickCheck +-- , numskull +--ghc-options: -Wall +-- -Wno-unused-top-binds +-- ghc-options: -Wno-unused-top-binds +-- default-language: Haskell2010 +-- +--test-suite doctest +-- import library-deps +-- type: exitcode-stdio-1.0 +-- hs-source-dirs: test, src +-- main-is: DocTest.hs +-- build-depends: base >=4.13.0.0 +-- , doctest +-- , vector +-- , split +-- ghc-options: -Wall +-- default-language: Haskell2010-- diff --git a/numskull.nix b/numskull.nix index 9a92d88..8f063b7 100644 --- a/numskull.nix +++ b/numskull.nix @@ -1,4 +1,4 @@ -{ mkDerivation, base, doctest, hspec, lib, QuickCheck, split +{ mkDerivation, base, lib, split , vector }: mkDerivation { @@ -7,7 +7,7 @@ mkDerivation { src = ./.; libraryHaskellDepends = [ base split vector ]; testHaskellDepends = [ - base doctest hspec QuickCheck split vector + base split vector ]; license = lib.licenses.mit; } \ No newline at end of file diff --git a/src/Indexing.hs b/src/Indexing.hs index f4bd51f..76b53c3 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -1,6 +1,8 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} module Indexing where @@ -9,12 +11,16 @@ import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import qualified Data.Map as M import Type.Reflection +import Text.ParserCombinators.Parsec import NdArray import Typing import qualified DType import DType (DType) import NdArrayException +import QuasiSlice +import QuasiSlice.Quote + {- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the @@ -89,7 +95,7 @@ value for the array e.g. 0. To avoid this use !?. -- >>> m #! [50] :: Int -- 0 (#!) :: DType a => NdArray -> [Integer] -> a -(NdArray s v) #! i = case NdArray s v !? i of +(NdArray s v) #! i = case NdArray s v #? i of Just val -> val Nothing -> DType.addId :: DType a => a @@ -99,8 +105,8 @@ value for the array e.g. 0. To avoid this use !?. -- Just 4 -- >>> m !? [50] :: Maybe Int -- Nothing -(!?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a -(NdArray s v) !? i = +(#?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a +(NdArray s v) #? i = let -- Converts any negative indices to their equivalent positives positives = zipWith positiveInd s i @@ -113,28 +119,34 @@ value for the array e.g. 0. To avoid this use !?. Nothing -> Nothing else Nothing +-- Converts negative indicies to their positive equivalents, counting back +-- from the end of the array (i.e. -1 is the last element). +positiveInd :: (Ord a, Num a) => a -> a -> a +positiveInd s i = if i < 0 then s+i else i + -- * SLICING --- | Type which allows you to provide only a single index or a range of indices. -data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) +positiveRanges :: [Integer] -> [(Integer, Integer)] -> [(Integer, Integer)] +positiveRanges = zipWith (\s (x,y) -> (positiveInd s x, if y < 0 then s+y else y-1)) + +-- Converts an IndexRange to a range of indicies in the standard pair form. +forceRange :: Integer -> IndexRange -> (Integer, Integer) +forceRange sh (I i) = (positiveInd sh i, positiveInd sh i) +forceRange sh (R s t) = (positiveInd sh s, if t < 0 then positiveInd sh t else t-1) --- | Integrated indexing and slicing. For each dimension you can provide either a single value +-- | The concise operator for slicing. Instead of providing an IndexRange, +-- You may QuasiQuote a NumPy-like index e.g. myArray /! [q|5,2:6,:3|]. +-- Unspecified values in ranges denote the start/end. +(/!) :: NdArray -> QuasiSlice -> NdArray +(/!) nd sl = nd #!+ (evalSlice sl) + +-- Integrated indexing and slicing. For each dimension you can provide either a single value -- or a range of values where a slice will be taken. (#!+) :: NdArray -> [IndexRange] -> NdArray (#!+) nd irs = slice (map forceRange irs) nd --(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) -- where (m,_) = mapIndices sh --- Converts an IndexRange to a range of indices in the standard pair form. -forceRange :: IndexRange -> (Integer, Integer) -forceRange (I i) = (i,i) -forceRange (R s t) = (s,t) - --- Converts negative indices to their positive equivalents, counting back --- from the end of the array (i.e. -1 is the last element). -positiveInd :: (Ord a, Num a) => a -> a -> a -positiveInd s i = if i < 0 then s+i else i - {- | Takes a series of ranges corresponding to each dimension in the array and returns the sub-array. Indicies are inclusive and can be negative. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray @@ -145,16 +157,6 @@ slice sl (NdArray s v) = flatinds = V.fromList $ map (fromInteger @Int . collapseInd s) inds newshape = map (\(x,y) -> y-x+1) sl' in NdArray newshape $ V.map (v V.!) flatinds -{- - collapse index ober the zio of the fsts and snds - let - ranges = zipWith (\(i,j) t -> [t*i, t*(i+1) .. t*j]) sl (V.toList st) - indicies = V.fromList $ (map sum . sequence) ranges - sh' = V.fromList $ map (\(i,j) -> j-i+1) sl - v' = V.map (v V.!) indicies - in - NdArray sh' (defStride sh') v' --} {- | Takes a series of ranges corresponding to each dimension in the array and returns the sub-array. Indicies are inclusive and can be negative. -} @@ -163,8 +165,8 @@ the sub-array. Indicies are inclusive and can be negative. -} -- where (m,_) = mapIndices sh -- | Equivalent slicing operator. -(!/) :: NdArray -> [(Integer, Integer)] -> NdArray -(!/) nd ss = slice ss nd +--(!/) :: NdArray -> [(Integer, Integer)] -> NdArray +--(!/) nd ss = slice ss nd {- -- Takes a slice on an NdArray given the mapping from the vector index to NdArray index. @@ -175,21 +177,20 @@ sliceWithMap _ d _ (NdArray sh v) | d >= length sh = NdArray sh v sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ sliceDim s d m (NdArray sh v) --- Takes a slice of an NdArray at a particular dimension. +-- Takes a slice of an NdArray at a particular dimension, no -ve indices. sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray sliceDim (x,y) d m (NdArray sh v) = if d >= length sh then throw (ExceededShape (fromIntegral d) sh) else NdArray - (if y' < x' then [] else shrinkNth d (y'-x'+1) sh) + (if y < x then [] else shrinkNth d (y-x+1) sh) (V.ifilter (\i _ -> let dimInd = (m M.! i) !! d - in x' <= dimInd && dimInd <= y') + in x <= dimInd && dimInd <= y) v ) where dimSize = sh !! d - (x', y') = (positiveInd dimSize x, positiveInd dimSize y) -- Replaces the nth value of an array if the newValue is smaller. -- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell diff --git a/src/Numskull.hs b/src/Numskull.hs index 6823065..581f545 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -4,9 +4,11 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE QuasiQuotes #-} module Numskull ( - -- Metadata + -- * Metadata DType , size , shape @@ -15,11 +17,11 @@ module Numskull ( , checkNdType , isEmpty - -- Creation + -- * Creation , NdArray , fromList , fromListFlat - , TreeMatrix + , TreeMatrix (..) , fromMatrix , fromVector , singleton @@ -27,7 +29,10 @@ module Numskull ( , zeros , squareArr - -- General mapping, folding & zipping + -- * Modification + , update + + -- * General Mapping, Folding & Zipping , foldrA , mapA , mapTransform @@ -35,12 +40,12 @@ module Numskull ( , pointwiseBool , zipArrayWith - -- Summaries + -- * Summaries , origin , maxElem , minElem - -- Mathematical constant + -- * Mathematical constant , scale , absA , signumA @@ -53,7 +58,7 @@ module Numskull ( , shiftleftA , shiftrightA - -- Mathematical pointwise + -- * Mathematical pointwise , elemDivide , elemDiv , elemPow @@ -61,17 +66,17 @@ module Numskull ( , Numskull.sum , mean - -- Bounds + -- * Bounds , clip - -- Type Conversions + -- * Type Conversions , convertDTypeTo , matchDType - -- Size conversions + -- * Size Conversions , resize - -- Shape conversions/manipulations + -- * Shape Conversions/Manipulations , reshape , padShape , constrainShape @@ -79,13 +84,13 @@ module Numskull ( , concatAlong , gather - -- Matrix manipulation + -- * Matrix Manipulation , swapRows , diagonal , transpose , transposePerm - --Matrix multiplication + -- * Matrix Multiplication , dot , matMul , upperTriangle @@ -94,29 +99,28 @@ module Numskull ( , swapRowsWith0Pivot , gemm - -- Indexing - , IndexRange + -- * Indexing , collapseInd , expandInd , map1DIndex , validIndex , (#!) - , (!?) - , (#!+) + , (#?) , slice - , (!/) + , (/!) + , evalSlice + , q - -- Pretty printing + -- * Pretty Printing , printArray , prettyShowArray - -- typing + -- Typing , (=@=) - -- numpy serialisation + -- Numpy Serialisation , saveNpy , loadNpy - ) where import qualified DType @@ -127,6 +131,8 @@ import NdArray import NdArrayException import Serialisation import Typing +import QuasiSlice +import QuasiSlice.Quote import Control.Exception import Control.Monad (zipWithM) @@ -304,6 +310,16 @@ zeros _ s = NdArray s zerovec ident = DType.addId :: (DType a => a) zerovec = V.replicate (size s) ident :: DType a => Vector a +update :: forall a . DType a => NdArray -> [Integer] -> a -> NdArray +update (NdArray s v) ind val = + NdArray s $ V.force (v V.// [(ind', val')]) + where + ind' = fromIntegral $ collapseInd s ind :: Int + val' = matchVecType v val + +matchVecType :: forall a b . (DType a, DType b) => Vector a -> b -> a +matchVecType _ x = DType.rationalToDtype (DType.dtypeToRational x) :: a + -- * Pointwise Functions -------------------------------------------------------------------------------- @@ -813,9 +829,9 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -- * Multiplication --- | Dot product over matrices of the same shape. -dot :: DType a => NdArray -> NdArray -> a -dot nd1 nd2 = foldrA DType.add DType.addId (nd1*nd2) +-- | Dot product over matricies of the same shape. +dot :: forall a. DType a => NdArray -> NdArray -> a +dot (NdArray s v) nd2 = foldrA DType.add (identityElem v <-@ typeRep @a) ((NdArray s v)*nd2) -- | Standard matrix multiplication following NumPy conventions. -- 1D arrays have the extra dimension pre/appended @@ -827,20 +843,20 @@ matMul (NdArray s v) (NdArray r u) = Just HRefl -> case (reverse s, reverse r) of -- Standard matrix multiplication - ([m, n], [q, p]) | m == p -> NdArray [n,q] (matMulVec s v r u) + ([m, n], [o, p]) | m == p -> NdArray [n,o] (matMulVec s v r u) -- 1D arrays have the extra dimension pre/appended then result collapses back to 1D - ([m], [q, p]) | m == p -> NdArray [q] (matMulVec [1,m] v r u) + ([m], [o, p]) | m == p -> NdArray [o] (matMulVec [1,m] v r u) ([m, n], [p]) | m == p -> NdArray [n] (matMulVec s v [p,1] u) -- ND-arrays are broadcast to match each other where possible and treated as -- stacks of nxm/pxq arrays. - (m : n : _, q : p : _) | m == p -> + (m : n : _, o : p : _) | m == p -> let (s', v', _r', u') = broadcastDimensions s v r u stackA = vectorChunksOf (fromIntegral @Integer @Int $ m * n) v' - stackB = vectorChunksOf (fromIntegral @Integer @Int $ q * p) u' - stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,q]) stackB + stackB = vectorChunksOf (fromIntegral @Integer @Int $ o * p) u' + stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,o]) stackB in - NdArray (take (length s' -2) s' ++ [n,q]) $ V.concat stackAB + NdArray (take (length s' -2) s' ++ [n,o]) $ V.concat stackAB _ -> throw (ShapeMismatch (NdArray s v) (NdArray r u) "matMul") _ -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "matMul") @@ -980,8 +996,8 @@ If the matrix is non-square it is assumed to be padded out and will have determi determinant :: forall a . DType a => NdArray -> [a] determinant (NdArray s v) = case s of [] -> [] - [_] -> [DType.addId :: a] - [_,_] -> [determinant2D (NdArray s v)] + [_] -> [identityElem v <-@ typeRep @a] + [_,_] -> [determinant2D (NdArray s v)] :: [a] _ | V.null v -> [] _ -> let @@ -996,9 +1012,9 @@ https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/M determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = case shape nd of - -- 2x2 matrices are calculated quickly with the standard ad-bc - [2,2] -> determinant2x2 nd - -- nxn matrices are row-swapped to find an arrangement with no zeros/identity elements + -- 2x2 matricies are calculated quickly with the standard ad-bc + [2,2] -> determinant2x2 nd :: a + -- nxn matricies are row-swapped to find an arrangement with no zeros/identity elements -- in the leading diagonal (pivots) then put into upper triangle form [c,r] | c == r && not (zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> @@ -1008,10 +1024,10 @@ determinant2D nd = pivots = diagonalVec s upperTriV in -- determinant is the product of the pivots in upper triangle form - V.foldr DType.multiply (DType.multId :: a) pivots + V.foldr DType.multiply (DType.multId :: a) pivots :: a -- If the matrix is non-square or has a zero-row/column, it is singular. - Nothing -> DType.addId - [_,_] -> DType.addId + Nothing -> DType.addId :: a + [_,_] -> DType.addId :: a _ -> error "Given matrix is not 2D." -- 2x2 quick determinant calculation of ad-bc diff --git a/src/QuasiSlice.hs b/src/QuasiSlice.hs new file mode 100644 index 0000000..f4dd06f --- /dev/null +++ b/src/QuasiSlice.hs @@ -0,0 +1,145 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE QuasiQuotes #-} + +module QuasiSlice (IndexRange(..), + QuasiSlice(..), + evalSlice, + parseSlice) +where + +import Text.ParserCombinators.Parsec +import Data.Typeable +import Data.Data + +-- | Type which allows you to provide only a single index or a range of indicies. +data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) + +data QuasiSlice = + NoIndexEx + | IndexEx Integer + | NegIndexEx Integer + | AntiIndexEx Bool String + | SliceEx QuasiSlice QuasiSlice + -- | AntiSliceExpr String + | CommaEx QuasiSlice QuasiSlice + -- | AntiCommaExpr String + deriving(Show, Typeable, Data) + +evalBound :: Bool -> QuasiSlice -> Integer +evalBound False NoIndexEx = 0 +evalBound True NoIndexEx = -1 +evalBound _ (IndexEx i) = i +evalBound _ (NegIndexEx i) = -i + +evalSlice :: QuasiSlice -> [IndexRange] +evalSlice x = case x of + NoIndexEx -> [R 0 (-1)] + IndexEx i -> [I i] + NegIndexEx i -> [I (-i)] + SliceEx l r -> [R (evalBound False l) (evalBound True r)] + CommaEx ex1 ex2 -> evalSlice ex1 ++ evalSlice ex2 + +------------ PARSER + +lexeme p = do{ x <- p; spaces; return x } +symbol name = lexeme (string name) + +comma = do{ symbol ","; return $ CommaEx } + +indiciesExpr :: CharParser st QuasiSlice +indiciesExpr = sliceIndex `chainl1` comma + +number :: CharParser st QuasiSlice +number = do + m <- optionMaybe $ symbol "-" + ds <- many digit + case (m, ds) of + (Nothing, []) -> try antiIntExpr <|> pure NoIndexEx + (Nothing, _) -> pure $ IndexEx (read ds) + (Just _, []) -> try (fmap antiNeg antiIntExpr) <|> pure NoIndexEx + (Just _, _) -> pure $ IndexEx (negate $ read ds) + where + antiNeg (AntiIndexEx _ x) = AntiIndexEx False x + +{- + == [] + then try anti <|> Nothing + else Just (read ds) + pure $ case m of + Nothing -> n + _ -> fmap negate n +-} + +sliceIndex :: CharParser st QuasiSlice +sliceIndex = lexeme $ do + l <- number + s <- optionMaybe $ symbol ":" + r <- number + case s of + Nothing -> pure l + Just _ -> pure $ SliceEx l r + +{- +sliceIndex :: CharParser st QuasiSlice +sliceIndex = sliceExpr <|> indexExpr + +index :: CharParser st QuasiSlice +index = lexeme $ do + ds <- many1 digit + --let i = if ds == [] then Nothing else Just (read ds) + return $ IndexExpr $ Just (read ds) + +sliceExpr :: CharParser st QuasiSlice +sliceExpr = try $ lexeme $ do + d1s <- many digit + symbol ":" + d2s <- many digit + let l = if d1s == [] then Nothing else Just (read d1s) + let r = if d2s == [] then Nothing else Just (read d2s) + return $ SliceExpr l r +-} +{- +indexEmpty :: CharParser st QuasiSlice +indexEmpty = lexeme $ do{ return $ IndexExpr Nothing } + +sliceLeft :: CharParser st QuasiSlice +sliceLeft = lexeme $ do{ ds <- many1 digit ; symbol ":" ; return $ SliceExpr (Just $ read ds) Nothing } + +sliceRight :: CharParser st QuasiSlice +sliceRight = lexeme $ do{ symbol ":" ; ds <- many1 digit ; return $ SliceExpr Nothing (Just $ read ds) } + +sliceEmpty :: CharParser st QuasiSlice +sliceEmpty = lexeme $ do{ symbol ":"; return $ SliceExpr Nothing Nothing } +-} + +small = lower <|> char '_' +large = upper +idchar = small <|> large <|> digit <|> char '\'' + +ident :: CharParser s String +ident = do{ c <- small; cs <- many idchar; return (c:cs) } + +-- To include variables in scope not just integers +antiIntExpr = lexeme $ do{ id <- ident; return $ AntiIndexEx True id } +--antiIntExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiIndexEx id } +--antiExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiExpr id } + +--------------- + +parseSlice :: (Monad m, MonadFail m) => (String, Int, Int) -> String -> m QuasiSlice +parseSlice (file, line, col) s = + case runParser p () "" s of + Left err -> fail $ show err + Right e -> return e + where + p = do pos <- getPosition + setPosition $ + (flip setSourceName) file $ + (flip setSourceLine) line $ + (flip setSourceColumn) col $ + pos + spaces + e <- indiciesExpr + eof + return e \ No newline at end of file diff --git a/src/QuasiSlice/Quote.hs b/src/QuasiSlice/Quote.hs new file mode 100644 index 0000000..784d6b9 --- /dev/null +++ b/src/QuasiSlice/Quote.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE QuasiQuotes #-} + +module QuasiSlice.Quote (q) where + +--import Data.Generics +import qualified Language.Haskell.TH as TH +import Language.Haskell.TH.Quote +import Data.Typeable +--import Language.Haskell.TH.Syntax(liftData) + +import QuasiSlice + +extQ :: ( Typeable a, Typeable b) => (a -> r) -> (b -> r) -> a -> r +extQ f g a = maybe (f a) g (cast a) + +q :: QuasiQuoter +q = QuasiQuoter { quoteExp = quoteExprExp +-- , quotePat = quoteExprPat + -- with ghc >= 7.4, you could also + -- define quoteType and quoteDec for + -- quasiquotes in those places too + } +------- + +quoteExprExp :: String -> TH.ExpQ +quoteExprExp s = do loc <- TH.location + let pos = (TH.loc_filename loc, + fst (TH.loc_start loc), + snd (TH.loc_start loc)) + expr <- parseSlice pos s + --dataToExpQ (\x -> Nothing) expr + --liftData expr + dataToExpQ (const Nothing `extQ` antiExprExp) expr + +antiExprExp :: QuasiSlice -> Maybe (TH.Q TH.Exp) +antiExprExp (AntiIndexEx True v) = Just $ TH.appE (TH.conE (TH.mkName "IndexEx")) + (TH.varE (TH.mkName v)) +antiExprExp (AntiIndexEx False v) = Just $ TH.appE (TH.conE (TH.mkName "NegIndexEx")) + (TH.varE (TH.mkName v)) +--antiExprExp (AntiExpr v) = Just $ TH.varE (TH.mkName v) +antiExprExp _ = Nothing + +------- +{- +quoteExprPat :: String -> TH.PatQ +quoteExprPat s = do loc <- TH.location + let pos = (TH.loc_filename loc, + fst (TH.loc_start loc), + snd (TH.loc_start loc)) + expr <- parseSlice pos s + dataToPatQ (const Nothing `extQ` antiExprPat) expr + +antiExprPat :: QuasiSlice -> Maybe (TH.Q TH.Pat) +antiExprPat (AntiIndexEx v) = Just $ TH.conP (TH.mkName "IndexEx") + [TH.varP (TH.mkName v)] +--antiExprPat (AntiExpr v) = Just $ TH.varP (TH.mkName v) +antiExprPat _ = Nothing +-} \ No newline at end of file diff --git a/src/testout/test123.npy b/src/testout/test123.npy deleted file mode 100644 index 643c53d3b67d826ec05c074c30dfe60ff01457a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlWC!@qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= dXCxM+0{I%oI+_Z#3X}jYMg|CAg3`=T8UW*c8(RPX diff --git a/test/Test/wiki/Expr.hs b/test/Test/wiki/Expr.hs new file mode 100644 index 0000000..e059109 --- /dev/null +++ b/test/Test/wiki/Expr.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Expr (Expr(..), + BinOp(..), + eval, + parseExpr) + where + +import Data.Generics +import Text.ParserCombinators.Parsec + +data Expr = IntExpr Integer + | AntiIntExpr String + | BinopExpr Expr Expr + | AntiExpr String + deriving(Show, Typeable, Data) + +data BinOp = AddOp + | SubOp + | MulOp + | DivOp + deriving(Show, Typeable, Data) + +eval :: Expr -> Integer +eval (IntExpr n) = n +eval (BinopExpr op x y) = (opToFun op) (eval x) (eval y) + where + opToFun AddOp = (+) + opToFun SubOp = (-) + opToFun MulOp = (*) + opToFun DivOp = div + + +------------ PARSER + +small = lower <|> char '_' +large = upper +idchar = small <|> large <|> digit <|> char '\'' + +lexeme p = do{ x <- p; spaces; return x } +symbol name = lexeme (string name) +parens p = between (symbol "(") (symbol ")") p + +expr :: CharParser st Expr +expr = term `chainl1` addop + +term :: CharParser st Expr +term = factor `chainl1` mulop + +factor :: CharParser st Expr +factor = parens expr <|> integer <|> try antiIntExpr <|> antiExpr + +mulop = do{ symbol "*"; return $ BinopExpr MulOp } + <|> do{ symbol "/"; return $ BinopExpr DivOp } + +addop = do{ symbol "+"; return $ BinopExpr AddOp } + <|> do{ symbol "-"; return $ BinopExpr SubOp } + +integer :: CharParser st Expr +integer = lexeme $ do{ ds <- many1 digit ; return $ IntExpr (read ds) } + +ident :: CharParser s String +ident = do{ c <- small; cs <- many idchar; return (c:cs) } + +antiIntExpr = lexeme $ do{ symbol "$int:"; id <- ident; return $ AntiIntExpr id } +antiExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiExpr id } + +--------------- + +parseExpr :: (Monad m, MonadFail m) => (String, Int, Int) -> String -> m Expr +parseExpr (file, line, col) s = + case runParser p () "" s of + Left err -> fail $ show err + Right e -> return e + where + p = do pos <- getPosition + setPosition $ + (flip setSourceName) file $ + (flip setSourceLine) line $ + (flip setSourceColumn) col $ + pos + spaces + e <- expr + eof + return e \ No newline at end of file diff --git a/test/Test/wiki/Expr/Quote.hs b/test/Test/wiki/Expr/Quote.hs new file mode 100644 index 0000000..89e5cfa --- /dev/null +++ b/test/Test/wiki/Expr/Quote.hs @@ -0,0 +1,46 @@ +module Expr.Quote (expr) where + +import Data.Generics +import qualified Language.Haskell.TH as TH +import Language.Haskell.TH.Quote + +import Expr + +quoteExprExp :: String -> TH.ExpQ +quoteExprPat :: String -> TH.PatQ + +expr :: QuasiQuoter +expr = QuasiQuoter { quoteExp = quoteExprExp, + quotePat = quoteExprPat + -- with ghc >= 7.4, you could also + -- define quoteType and quoteDec for + -- quasiquotes in those places too + } +------- +quoteExprExp s = do loc <- TH.location + let pos = (TH.loc_filename loc, + fst (TH.loc_start loc), + snd (TH.loc_start loc)) + expr <- parseExpr pos s + dataToExpQ (const Nothing `extQ` antiExprExp) expr + +antiExprExp :: Expr -> Maybe (TH.Q TH.Exp) +antiExprExp (AntiIntExpr v) = Just $ TH.appE (TH.conE (TH.mkName "IntExpr")) + (TH.varE (TH.mkName v)) +antiExprExp (AntiExpr v) = Just $ TH.varE (TH.mkName v) +antiExprExp _ = Nothing + +------- +quoteExprPat s = do loc <- TH.location + let pos = (TH.loc_filename loc, + fst (TH.loc_start loc), + snd (TH.loc_start loc)) + expr <- parseExpr pos s + dataToPatQ (const Nothing `extQ` antiExprPat) expr + +antiExprPat :: Expr -> Maybe (TH.Q TH.Pat) +antiExprPat (AntiIntExpr v) = Just $ TH.conP (TH.mkName "IntExpr") + [TH.varP (TH.mkName v)] +antiExprPat (AntiExpr v) = Just $ TH.varP (TH.mkName v) +antiExprPat _ = Nothing + From d19b866b047446f062e5af9eb89f35c8cef4f2f4 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 15:01:03 +0100 Subject: [PATCH 74/90] updated readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index ba05c4d..09f1a78 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,24 @@ # rowan-numskull +## Using Numskull + +This is a Summer Internship project from 2023. Numskull is a NumPy-like library for Haskell, featuring NdArrays which can be created and manipulated to store many different types (of the DType class). + +Numskull was designed for purposes of integration into an [Onnx](https://onnx.ai/) backend, but it can be used anywhere you need to operate on arrays of unspecified type and shape. + +For more information, have a look at my talk: [slides](demo/presentation-slides.pdf). + +To run the demo you need +1) jupyter +2) iHaskell (https://github.com/IHaskell/IHaskell) to put Numskull +code into a jupyter notebook. +3) nix-shell +4) cd demo/notebook/ +5) ./start.sh + +Note that the work in main is Numskull 1.0. +Numskull 2.0 can be found in the so-called branch! The second version is less well tested and complete, but should be more efficient since it makes use of strides. I didn't have time to integrate that into the Onnx backend, but it shouldn't be at all difficult to do so. + ## Development ### Using Cabal From 8d7fe11dc6d0dfa76e39afabe2b84cbfa6558d63 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 16:27:22 +0100 Subject: [PATCH 75/90] slicing merged and corrected --- .vscode/settings.json | 3 +++ src/Indexing.hs | 17 +++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4e8191a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.mergeEditor": true +} \ No newline at end of file diff --git a/src/Indexing.hs b/src/Indexing.hs index 76b53c3..2597c06 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -143,32 +143,33 @@ forceRange sh (R s t) = (positiveInd sh s, if t < 0 then positiveInd sh t else t -- Integrated indexing and slicing. For each dimension you can provide either a single value -- or a range of values where a slice will be taken. (#!+) :: NdArray -> [IndexRange] -> NdArray -(#!+) nd irs = slice (map forceRange irs) nd ---(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) --- where (m,_) = mapIndices sh +(#!+) (NdArray s v) irs = slice (zipWith forceRange s irs) (NdArray s v) {- | Takes a series of ranges corresponding to each dimension in the array and returns the sub-array. Indicies are inclusive and can be negative. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray slice sl (NdArray s v) = let - sl' = zipWith (\(x,y) sk -> (positiveInd sk x, positiveInd sk y)) sl s + pad = sl ++ replicate (length s - length sl) (0,-1) + sl' = zipWith (\(x,y) sk -> (positiveInd sk x, positiveInd sk y)) pad s inds = sequence $ map (\(x, y) -> [x..y]) sl' flatinds = V.fromList $ map (fromInteger @Int . collapseInd s) inds newshape = map (\(x,y) -> y-x+1) sl' in NdArray newshape $ V.map (v V.!) flatinds -{- | Takes a series of ranges corresponding to each dimension in the array and returns -the sub-array. Indicies are inclusive and can be negative. -} +{- +slicing with maps --slice :: [(Integer, Integer)] -> NdArray -> NdArray --slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) -- where (m,_) = mapIndices sh --- | Equivalent slicing operator. +--(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) +-- where (m,_) = mapIndices sh + +-- Equivalent slicing operator. --(!/) :: NdArray -> [(Integer, Integer)] -> NdArray --(!/) nd ss = slice ss nd -{- -- Takes a slice on an NdArray given the mapping from the vector index to NdArray index. -- Iterates through each dimension of the slice one at a time. sliceWithMap :: M.Map Int [Integer] -> Int -> [(Integer, Integer)] -> NdArray -> NdArray From 0d9d079508666bb5c97eea2567e1a403c3d0d9c7 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 17:02:37 +0100 Subject: [PATCH 76/90] tidying --- .gitignore | 3 +- {ihaskell => demo}/default.nix | 2 +- .../Rowan-Presentation-checkpoint.ipynb | 0 demo/notebook/Rowan-Presentation.ipynb | 241 +++++++++ {ihaskell/demo => demo/notebook}/default.nix | 0 .../notebook}/serialisationdemo.npy | Bin {ihaskell/demo => demo/notebook}/start.sh | 0 {ihaskell => demo}/pkgs.nix | 0 {ihaskell => demo}/rise.nix | 0 {ihaskell => demo}/updater | 0 {ihaskell => demo}/versions.json | 0 ihaskell/demo/Rowan-Presentation.ipynb | 502 ------------------ numskull.cabal | 27 - src/QuasiSlice.hs | 52 +- src/Serialisation.hs | 11 - src/Test.hs | 12 - test/Test/upper_triangle.py | 19 - test/Test/wiki/Expr.hs | 84 --- test/Test/wiki/Expr/Quote.hs | 46 -- 19 files changed, 248 insertions(+), 751 deletions(-) rename {ihaskell => demo}/default.nix (90%) rename {ihaskell/demo => demo/notebook}/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb (100%) create mode 100644 demo/notebook/Rowan-Presentation.ipynb rename {ihaskell/demo => demo/notebook}/default.nix (100%) rename {ihaskell/demo => demo/notebook}/serialisationdemo.npy (100%) rename {ihaskell/demo => demo/notebook}/start.sh (100%) rename {ihaskell => demo}/pkgs.nix (100%) rename {ihaskell => demo}/rise.nix (100%) rename {ihaskell => demo}/updater (100%) rename {ihaskell => demo}/versions.json (100%) delete mode 100644 ihaskell/demo/Rowan-Presentation.ipynb delete mode 100644 src/Test.hs delete mode 100644 test/Test/upper_triangle.py delete mode 100644 test/Test/wiki/Expr.hs delete mode 100644 test/Test/wiki/Expr/Quote.hs diff --git a/.gitignore b/.gitignore index 67855a0..df02d62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist-newstyle/ -result/ \ No newline at end of file +result/ +.vscode/ \ No newline at end of file diff --git a/ihaskell/default.nix b/demo/default.nix similarity index 90% rename from ihaskell/default.nix rename to demo/default.nix index 469aa13..b4fc5d3 100644 --- a/ihaskell/default.nix +++ b/demo/default.nix @@ -6,5 +6,5 @@ let path = import (./. + "/${folder}"); }); in nixpkgs.linkFarm "notebooks" (notebooks [ - "codensity", + "Rowan-Presentation" ]) diff --git a/ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb b/demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb similarity index 100% rename from ihaskell/demo/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb rename to demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb diff --git a/demo/notebook/Rowan-Presentation.ipynb b/demo/notebook/Rowan-Presentation.ipynb new file mode 100644 index 0000000..147627f --- /dev/null +++ b/demo/notebook/Rowan-Presentation.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Numskull Demo!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ":1:5: error:\n • Can't find interface-file declaration for variable printArray\n Probable cause: bug in .hi-boot file, or inconsistent .hi file\n Use -ddump-if-trace to get an idea of which file caused the error\n • In the expression: printArray\n In an equation for ‘p’: p = printArray" + ] + } + ], + "source": [ + "{-# LANGUAGE TypeApplications #-}\n", + "{-# LANGUAGE TemplateHaskell #-}\n", + "{-# LANGUAGE QuasiQuotes #-}\n", + "import Numskull\n", + "import Data.Maybe (fromJust)\n", + "import Type.Reflection\n", + "\n", + "p = printArray" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It's easy to make arrays." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ":1:1: error: Variable not in scope: p :: NdArray -> t" + ] + } + ], + "source": [ + "p $ fromList [3] [2,4,6]\n", + "\n", + "p $ singleton 3.14\n", + "\n", + "p.fromJust $ reshape [4,5] $ arange 1 (20::Int)\n", + "\n", + "p $ zeros (typeRep @Int) [3,3]\n", + "\n", + "l :: TreeMatrix Int\n", + "l = A [A [B 1, B 2],\n", + " A [B 3, B 4],\n", + " A [B 5, B 6]]\n", + "p $ fromMatrix l" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or take slices of them..." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" + ] + } + ], + "source": [ + "piNd = fromList [2,3,3] [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3::Int]\n", + "\n", + "putStrLn \"3D Array:\"\n", + "p piNd\n", + "\n", + "putStrLn \"Sliced:\"\n", + "p $ slice [(0,0), (1,2)] piNd\n", + "\n", + "putStrLn \"Sliced, but fancier:\"\n", + "p $ piNd /! [q|0,1:2|]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And switch values or even types" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" + ] + } + ], + "source": [ + "intNd = fromListFlat [1, 3, 6, 10, 15, 21, 28, 36, 45 :: Int]\n", + "boolNd = fromListFlat [True, True, False, True]\n", + "\n", + "p $ update intNd [0] 100\n", + "\n", + "p $ convertDTypeTo (typeRep @Double) intNd\n", + "p $ matchDType intNd boolNd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And do all sorts of fun maths!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" + ] + } + ], + "source": [ + "nd1 = fromList [3,3] [0..8::Int]\n", + "nd2 = padShape [3,3] $ fromList [2,2] [10,10,10,10::Int]\n", + "\n", + "putStrLn \"Numeracy:\"\n", + "p $ nd1 + nd2\n", + "p $ Numskull.sum [nd1, nd2]\n", + "p $ nd1 * nd2\n", + "\n", + "putStrLn \"Powers/logs:\"\n", + "p $ elemPow nd1 (fromList [3,3] $ replicate 9 (2::Int))\n", + "\n", + "putStrLn \"Average:\"\n", + "p $ mean [nd1, nd2]\n", + "\n", + "putStrLn \"Transpose & diagonal:\"\n", + "p $ transpose nd1\n", + "p $ diagonal nd1\n", + "\n", + "putStrLn \"Matrix multiplication:\"\n", + "nd3 = fromList [2,2] [0..3::Float]\n", + "nd4 = fromList [2,2] [4..7::Float]\n", + "p $ matMul nd3 nd3\n", + "m = fromJust (gemm nd3 nd3 nd4 True False 3 1)\n", + "p m\n", + "\n", + "putStrLn \"Determinant:\"\n", + "print (determinant m :: [Float])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the built-in numskull operations aren't good enough for you, and you don't want to write your own, just use NumPy.\n", + "\n", + "NumSkull will serialise most standard DType arrays to NumPy .npy files and back. But you're just going to have to trust me a bit here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + ":1:1: error:\n • Can't find interface-file declaration for variable saveNpy\n Probable cause: bug in .hi-boot file, or inconsistent .hi file\n Use -ddump-if-trace to get an idea of which file caused the error\n • In the expression: saveNpy \"./serialisationdemo.npy\" nd1\n In an equation for ‘it’: it = saveNpy \"./serialisationdemo.npy\" nd1" + ] + } + ], + "source": [ + "saveNpy \"./serialisationdemo.npy\" nd1\n", + "loadNpy \"./serialisationdemo.npy\" >>= p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Haskell", + "language": "haskell", + "name": "haskell" + }, + "language_info": { + "codemirror_mode": "ihaskell", + "file_extension": ".hs", + "mimetype": "text/x-haskell", + "name": "haskell", + "pygments_lexer": "Haskell", + "version": "9.0.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ihaskell/demo/default.nix b/demo/notebook/default.nix similarity index 100% rename from ihaskell/demo/default.nix rename to demo/notebook/default.nix diff --git a/ihaskell/demo/serialisationdemo.npy b/demo/notebook/serialisationdemo.npy similarity index 100% rename from ihaskell/demo/serialisationdemo.npy rename to demo/notebook/serialisationdemo.npy diff --git a/ihaskell/demo/start.sh b/demo/notebook/start.sh similarity index 100% rename from ihaskell/demo/start.sh rename to demo/notebook/start.sh diff --git a/ihaskell/pkgs.nix b/demo/pkgs.nix similarity index 100% rename from ihaskell/pkgs.nix rename to demo/pkgs.nix diff --git a/ihaskell/rise.nix b/demo/rise.nix similarity index 100% rename from ihaskell/rise.nix rename to demo/rise.nix diff --git a/ihaskell/updater b/demo/updater similarity index 100% rename from ihaskell/updater rename to demo/updater diff --git a/ihaskell/versions.json b/demo/versions.json similarity index 100% rename from ihaskell/versions.json rename to demo/versions.json diff --git a/ihaskell/demo/Rowan-Presentation.ipynb b/ihaskell/demo/Rowan-Presentation.ipynb deleted file mode 100644 index 25b9c6b..0000000 --- a/ihaskell/demo/Rowan-Presentation.ipynb +++ /dev/null @@ -1,502 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Numskull Demo!" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "{-# LANGUAGE TypeApplications #-}\n", - "{-# LANGUAGE TemplateHaskell #-}\n", - "{-# LANGUAGE QuasiQuotes #-}\n", - "import Numskull\n", - "import Data.Maybe (fromJust)\n", - "import Type.Reflection\n", - "\n", - "p = printArray" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It's easy to make arrays." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "2.0 4.0 6.0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "3.14" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - " 1 2 3 4 5 \n", - " 6 7 8 9 10 \n", - "11 12 13 14 15 \n", - "16 17 18 19 20" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "0 0 0 \n", - "0 0 0 \n", - "0 0 0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "1 2 \n", - "3 4 \n", - "5 6" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "p $ fromList [3] [2,4,6]\n", - "\n", - "p $ singleton 3.14\n", - "\n", - "p.fromJust $ reshape [4,5] $ arange 1 (20::Int)\n", - "\n", - "p $ zeros (typeRep @Int) [3,3]\n", - "\n", - "l :: TreeMatrix Int\n", - "l = A [A [B 1, B 2],\n", - " A [B 3, B 4],\n", - " A [B 5, B 6]]\n", - "p $ fromMatrix l" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or take slices of them..." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "3D Array:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "3 1 4 \n", - "1 5 9 \n", - "2 6 5 \n", - "\n", - "3 5 8 \n", - "9 7 9 \n", - "3 2 3" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Sliced:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "1 5 9 \n", - "2 6 5" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Sliced, but fancier:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "1 5 9 \n", - "2 6 5" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "piNd = fromList [2,3,3] [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3::Int]\n", - "\n", - "putStrLn \"3D Array:\"\n", - "p piNd\n", - "\n", - "putStrLn \"Sliced:\"\n", - "p $ slice [(0,0), (1,2)] piNd\n", - "\n", - "putStrLn \"Sliced, but fancier:\"\n", - "p $ piNd /! [q|0,1:2|]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And switch values or even types" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "100 3 6 10 15 21 28 36 45" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - " 1.0 3.0 6.0 10.0 15.0 21.0 28.0 36.0 45.0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "1 1 0 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "intNd = fromListFlat [1, 3, 6, 10, 15, 21, 28, 36, 45 :: Int]\n", - "boolNd = fromListFlat [True, True, False, True]\n", - "\n", - "p $ update intNd [0] 100\n", - "\n", - "p $ convertDTypeTo (typeRep @Double) intNd\n", - "p $ matchDType intNd boolNd" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And do all sorts of fun maths!" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Numeracy:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "10 11 2 \n", - "13 14 5 \n", - " 6 7 8" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "10 11 2 \n", - "13 14 5 \n", - " 6 7 8" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - " 0 10 0 \n", - "30 40 0 \n", - " 0 0 0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Powers/logs:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - " 0 1 4 \n", - " 9 16 25 \n", - "36 49 64" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Average:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "5 5 1 \n", - "6 7 2 \n", - "3 3 4" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Transpose & diagonal:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "0 3 6 \n", - "1 4 7 \n", - "2 5 8" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "0 4 8" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Matrix multiplication:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - " 2.0 3.0 \n", - " 6.0 11.0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "16.0 23.0 \n", - "24.0 37.0" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Determinant:" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "[40.0]" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "nd1 = fromList [3,3] [0..8::Int]\n", - "nd2 = padShape [3,3] $ fromList [2,2] [10,10,10,10::Int]\n", - "\n", - "putStrLn \"Numeracy:\"\n", - "p $ nd1 + nd2\n", - "p $ Numskull.sum [nd1, nd2]\n", - "p $ nd1 * nd2\n", - "\n", - "putStrLn \"Powers/logs:\"\n", - "p $ elemPow nd1 (fromList [3,3] $ replicate 9 (2::Int))\n", - "\n", - "putStrLn \"Average:\"\n", - "p $ mean [nd1, nd2]\n", - "\n", - "putStrLn \"Transpose & diagonal:\"\n", - "p $ transpose nd1\n", - "p $ diagonal nd1\n", - "\n", - "putStrLn \"Matrix multiplication:\"\n", - "nd3 = fromList [2,2] [0..3::Float]\n", - "nd4 = fromList [2,2] [4..7::Float]\n", - "p $ matMul nd3 nd3\n", - "m = fromJust (gemm nd3 nd3 nd4 True False 3 1)\n", - "p m\n", - "\n", - "putStrLn \"Determinant:\"\n", - "print (determinant m :: [Float])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If the built-in numskull operations aren't good enough for you, and you don't want to write your own, just use NumPy.\n", - "\n", - "NumSkull will serialise most standard DType arrays to NumPy .npy files and back. But you're just going to have to trust me a bit here..." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 1 2 \n", - "3 4 5 \n", - "6 7 8" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "saveNpy \"./serialisationdemo.npy\" nd1\n", - "loadNpy \"./serialisationdemo.npy\" >>= p" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Haskell", - "language": "haskell", - "name": "haskell" - }, - "language_info": { - "codemirror_mode": "ihaskell", - "file_extension": ".hs", - "mimetype": "text/x-haskell", - "name": "haskell", - "pygments_lexer": "Haskell", - "version": "9.0.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/numskull.cabal b/numskull.cabal index 1449a04..721d77c 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -35,30 +35,3 @@ library hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall - ---test-suite test --- type: exitcode-stdio-1.0 --- hs-source-dirs: test --- main-is: Test.hs --- other-modules: Test.Numskull --- Test.Serialisation --- build-depends: base >=4.13.0.0 --- , hspec --- , QuickCheck --- , numskull ---ghc-options: -Wall --- -Wno-unused-top-binds --- ghc-options: -Wno-unused-top-binds --- default-language: Haskell2010 --- ---test-suite doctest --- import library-deps --- type: exitcode-stdio-1.0 --- hs-source-dirs: test, src --- main-is: DocTest.hs --- build-depends: base >=4.13.0.0 --- , doctest --- , vector --- , split --- ghc-options: -Wall --- default-language: Haskell2010-- diff --git a/src/QuasiSlice.hs b/src/QuasiSlice.hs index f4dd06f..7cef9b8 100644 --- a/src/QuasiSlice.hs +++ b/src/QuasiSlice.hs @@ -15,23 +15,24 @@ import Data.Data -- | Type which allows you to provide only a single index or a range of indicies. data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) +-- QuasiQuoted slices are converted to this to be evaluated. data QuasiSlice = NoIndexEx | IndexEx Integer | NegIndexEx Integer | AntiIndexEx Bool String | SliceEx QuasiSlice QuasiSlice - -- | AntiSliceExpr String | CommaEx QuasiSlice QuasiSlice - -- | AntiCommaExpr String deriving(Show, Typeable, Data) +-- Checks for the prescence of a value in a range e.g. ():4) evalBound :: Bool -> QuasiSlice -> Integer evalBound False NoIndexEx = 0 evalBound True NoIndexEx = -1 evalBound _ (IndexEx i) = i evalBound _ (NegIndexEx i) = -i +-- Converts the Quasi slice to an IndexRange which can be operated on as usual in Indexing. evalSlice :: QuasiSlice -> [IndexRange] evalSlice x = case x of NoIndexEx -> [R 0 (-1)] @@ -62,15 +63,6 @@ number = do where antiNeg (AntiIndexEx _ x) = AntiIndexEx False x -{- - == [] - then try anti <|> Nothing - else Just (read ds) - pure $ case m of - Nothing -> n - _ -> fmap negate n --} - sliceIndex :: CharParser st QuasiSlice sliceIndex = lexeme $ do l <- number @@ -80,39 +72,6 @@ sliceIndex = lexeme $ do Nothing -> pure l Just _ -> pure $ SliceEx l r -{- -sliceIndex :: CharParser st QuasiSlice -sliceIndex = sliceExpr <|> indexExpr - -index :: CharParser st QuasiSlice -index = lexeme $ do - ds <- many1 digit - --let i = if ds == [] then Nothing else Just (read ds) - return $ IndexExpr $ Just (read ds) - -sliceExpr :: CharParser st QuasiSlice -sliceExpr = try $ lexeme $ do - d1s <- many digit - symbol ":" - d2s <- many digit - let l = if d1s == [] then Nothing else Just (read d1s) - let r = if d2s == [] then Nothing else Just (read d2s) - return $ SliceExpr l r --} -{- -indexEmpty :: CharParser st QuasiSlice -indexEmpty = lexeme $ do{ return $ IndexExpr Nothing } - -sliceLeft :: CharParser st QuasiSlice -sliceLeft = lexeme $ do{ ds <- many1 digit ; symbol ":" ; return $ SliceExpr (Just $ read ds) Nothing } - -sliceRight :: CharParser st QuasiSlice -sliceRight = lexeme $ do{ symbol ":" ; ds <- many1 digit ; return $ SliceExpr Nothing (Just $ read ds) } - -sliceEmpty :: CharParser st QuasiSlice -sliceEmpty = lexeme $ do{ symbol ":"; return $ SliceExpr Nothing Nothing } --} - small = lower <|> char '_' large = upper idchar = small <|> large <|> digit <|> char '\'' @@ -120,11 +79,8 @@ idchar = small <|> large <|> digit <|> char '\'' ident :: CharParser s String ident = do{ c <- small; cs <- many idchar; return (c:cs) } --- To include variables in scope not just integers +-- To include variables in scope, not just integers antiIntExpr = lexeme $ do{ id <- ident; return $ AntiIndexEx True id } ---antiIntExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiIndexEx id } ---antiExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiExpr id } - --------------- parseSlice :: (Monad m, MonadFail m) => (String, Int, Int) -> String -> m QuasiSlice diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 82860c3..3939160 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -25,17 +25,6 @@ import Type.Reflection -- | Built in numpy serialisation descriptions getNumpyDType :: NdArray -> String -{- -getNumpyDType (NdArray _ v) = case show $ typeOf v of - "Vector Int" -> " " " " " " " error "Non-standard types cannot be serialised." --} getNumpyDType (NdArray _ v) | isType (typeRep @Int) = " [[Int]] -cartProdN = foldr - (\(l,u) as -> - [ x : a - | x <- [l..u] - , a <- as ]) - [[]] --} - -thing xs ys = sequence $ zipWith (\x y -> [x..y]) xs ys \ No newline at end of file diff --git a/test/Test/upper_triangle.py b/test/Test/upper_triangle.py deleted file mode 100644 index b2bd120..0000000 --- a/test/Test/upper_triangle.py +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np - -x = np.array([2,5,1, 9,2,7, 4,16,3], dtype=float).reshape(3,3) -print(x) - -#first index is y -print(x[1,2]) -print() -for i in range(3): - for j in range(i+1,3): - ratio = x[j,i] / x[i,i] - for k in range(3): - x[j,k] = x[j,k] - ratio * x[i,k] - print(i,j,k, ratio) - - -print() -print(x) -print(np.linalg.det(x)) diff --git a/test/Test/wiki/Expr.hs b/test/Test/wiki/Expr.hs deleted file mode 100644 index e059109..0000000 --- a/test/Test/wiki/Expr.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -module Expr (Expr(..), - BinOp(..), - eval, - parseExpr) - where - -import Data.Generics -import Text.ParserCombinators.Parsec - -data Expr = IntExpr Integer - | AntiIntExpr String - | BinopExpr Expr Expr - | AntiExpr String - deriving(Show, Typeable, Data) - -data BinOp = AddOp - | SubOp - | MulOp - | DivOp - deriving(Show, Typeable, Data) - -eval :: Expr -> Integer -eval (IntExpr n) = n -eval (BinopExpr op x y) = (opToFun op) (eval x) (eval y) - where - opToFun AddOp = (+) - opToFun SubOp = (-) - opToFun MulOp = (*) - opToFun DivOp = div - - ------------- PARSER - -small = lower <|> char '_' -large = upper -idchar = small <|> large <|> digit <|> char '\'' - -lexeme p = do{ x <- p; spaces; return x } -symbol name = lexeme (string name) -parens p = between (symbol "(") (symbol ")") p - -expr :: CharParser st Expr -expr = term `chainl1` addop - -term :: CharParser st Expr -term = factor `chainl1` mulop - -factor :: CharParser st Expr -factor = parens expr <|> integer <|> try antiIntExpr <|> antiExpr - -mulop = do{ symbol "*"; return $ BinopExpr MulOp } - <|> do{ symbol "/"; return $ BinopExpr DivOp } - -addop = do{ symbol "+"; return $ BinopExpr AddOp } - <|> do{ symbol "-"; return $ BinopExpr SubOp } - -integer :: CharParser st Expr -integer = lexeme $ do{ ds <- many1 digit ; return $ IntExpr (read ds) } - -ident :: CharParser s String -ident = do{ c <- small; cs <- many idchar; return (c:cs) } - -antiIntExpr = lexeme $ do{ symbol "$int:"; id <- ident; return $ AntiIntExpr id } -antiExpr = lexeme $ do{ symbol "$"; id <- ident; return $ AntiExpr id } - ---------------- - -parseExpr :: (Monad m, MonadFail m) => (String, Int, Int) -> String -> m Expr -parseExpr (file, line, col) s = - case runParser p () "" s of - Left err -> fail $ show err - Right e -> return e - where - p = do pos <- getPosition - setPosition $ - (flip setSourceName) file $ - (flip setSourceLine) line $ - (flip setSourceColumn) col $ - pos - spaces - e <- expr - eof - return e \ No newline at end of file diff --git a/test/Test/wiki/Expr/Quote.hs b/test/Test/wiki/Expr/Quote.hs deleted file mode 100644 index 89e5cfa..0000000 --- a/test/Test/wiki/Expr/Quote.hs +++ /dev/null @@ -1,46 +0,0 @@ -module Expr.Quote (expr) where - -import Data.Generics -import qualified Language.Haskell.TH as TH -import Language.Haskell.TH.Quote - -import Expr - -quoteExprExp :: String -> TH.ExpQ -quoteExprPat :: String -> TH.PatQ - -expr :: QuasiQuoter -expr = QuasiQuoter { quoteExp = quoteExprExp, - quotePat = quoteExprPat - -- with ghc >= 7.4, you could also - -- define quoteType and quoteDec for - -- quasiquotes in those places too - } -------- -quoteExprExp s = do loc <- TH.location - let pos = (TH.loc_filename loc, - fst (TH.loc_start loc), - snd (TH.loc_start loc)) - expr <- parseExpr pos s - dataToExpQ (const Nothing `extQ` antiExprExp) expr - -antiExprExp :: Expr -> Maybe (TH.Q TH.Exp) -antiExprExp (AntiIntExpr v) = Just $ TH.appE (TH.conE (TH.mkName "IntExpr")) - (TH.varE (TH.mkName v)) -antiExprExp (AntiExpr v) = Just $ TH.varE (TH.mkName v) -antiExprExp _ = Nothing - -------- -quoteExprPat s = do loc <- TH.location - let pos = (TH.loc_filename loc, - fst (TH.loc_start loc), - snd (TH.loc_start loc)) - expr <- parseExpr pos s - dataToPatQ (const Nothing `extQ` antiExprPat) expr - -antiExprPat :: Expr -> Maybe (TH.Q TH.Pat) -antiExprPat (AntiIntExpr v) = Just $ TH.conP (TH.mkName "IntExpr") - [TH.varP (TH.mkName v)] -antiExprPat (AntiExpr v) = Just $ TH.varP (TH.mkName v) -antiExprPat _ = Nothing - From 8846b5002ca9c2728a7459a87ca1298108d761e7 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 24 Aug 2023 17:19:19 +0100 Subject: [PATCH 77/90] Add files via upload --- .../Rowan-Presentation-checkpoint.ipynb | 6 +- demo/notebook/Rowan-Presentation.ipynb | 355 +++++++++++++++--- demo/presentation-slides.pdf | Bin 0 -> 931030 bytes numskull.cabal | 12 +- 4 files changed, 317 insertions(+), 56 deletions(-) create mode 100644 demo/presentation-slides.pdf diff --git a/demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb b/demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb index 25b9c6b..678c23f 100644 --- a/demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb +++ b/demo/notebook/.ipynb_checkpoints/Rowan-Presentation-checkpoint.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -113,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -189,7 +189,7 @@ "p $ slice [(0,0), (1,2)] piNd\n", "\n", "putStrLn \"Sliced, but fancier:\"\n", - "p $ piNd /! [q|0,1:2|]" + "p $ piNd /! [q|0,1:3|]" ] }, { diff --git a/demo/notebook/Rowan-Presentation.ipynb b/demo/notebook/Rowan-Presentation.ipynb index 147627f..678c23f 100644 --- a/demo/notebook/Rowan-Presentation.ipynb +++ b/demo/notebook/Rowan-Presentation.ipynb @@ -9,18 +9,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, - "outputs": [ - { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ":1:5: error:\n • Can't find interface-file declaration for variable printArray\n Probable cause: bug in .hi-boot file, or inconsistent .hi file\n Use -ddump-if-trace to get an idea of which file caused the error\n • In the expression: printArray\n In an equation for ‘p’: p = printArray" - ] - } - ], + "outputs": [], "source": [ "{-# LANGUAGE TypeApplications #-}\n", "{-# LANGUAGE TemplateHaskell #-}\n", @@ -41,16 +32,60 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ":1:1: error: Variable not in scope: p :: NdArray -> t" - ] + "data": { + "text/plain": [ + "2.0 4.0 6.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3.14" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1 2 3 4 5 \n", + " 6 7 8 9 10 \n", + "11 12 13 14 15 \n", + "16 17 18 19 20" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 0 0 \n", + "0 0 0 \n", + "0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 2 \n", + "3 4 \n", + "5 6" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -78,16 +113,70 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" - ] + "data": { + "text/plain": [ + "3D Array:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "3 1 4 \n", + "1 5 9 \n", + "2 6 5 \n", + "\n", + "3 5 8 \n", + "9 7 9 \n", + "3 2 3" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Sliced, but fancier:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 5 9 \n", + "2 6 5" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -100,7 +189,7 @@ "p $ slice [(0,0), (1,2)] piNd\n", "\n", "putStrLn \"Sliced, but fancier:\"\n", - "p $ piNd /! [q|0,1:2|]" + "p $ piNd /! [q|0,1:3|]" ] }, { @@ -112,16 +201,35 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" - ] + "data": { + "text/plain": [ + "100 3 6 10 15 21 28 36 45" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 1.0 3.0 6.0 10.0 15.0 21.0 28.0 36.0 45.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "1 1 0 1" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -143,16 +251,166 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ": /nix/store/llzcby92zdn7wwawj50y90abjz1hzlr0-numskull-0.1.0.0/lib/ghc-9.0.2/x86_64-linux-ghc-9.0.2/libHSnumskull-0.1.0.0-Jzem7IPC4coA4KXysvkrJd-ghc9.0.2.so: undefined symbol: numskullzm0zi1zi0zi0zmJzzem7IPC4coA4KXysvkrJd_Indexing_slice_info" - ] + "data": { + "text/plain": [ + "Numeracy:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "10 11 2 \n", + "13 14 5 \n", + " 6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 10 0 \n", + "30 40 0 \n", + " 0 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Powers/logs:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 0 1 4 \n", + " 9 16 25 \n", + "36 49 64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Average:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "5 5 1 \n", + "6 7 2 \n", + "3 3 4" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Transpose & diagonal:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 3 6 \n", + "1 4 7 \n", + "2 5 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0 4 8" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Matrix multiplication:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + " 2.0 3.0 \n", + " 6.0 11.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "16.0 23.0 \n", + "24.0 37.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Determinant:" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[40.0]" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ @@ -196,16 +454,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [ { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - ":1:1: error:\n • Can't find interface-file declaration for variable saveNpy\n Probable cause: bug in .hi-boot file, or inconsistent .hi file\n Use -ddump-if-trace to get an idea of which file caused the error\n • In the expression: saveNpy \"./serialisationdemo.npy\" nd1\n In an equation for ‘it’: it = saveNpy \"./serialisationdemo.npy\" nd1" - ] + "data": { + "text/plain": [ + "0 1 2 \n", + "3 4 5 \n", + "6 7 8" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ diff --git a/demo/presentation-slides.pdf b/demo/presentation-slides.pdf new file mode 100644 index 0000000000000000000000000000000000000000..51841789ab0428649e56790de3e4ec739cc52095 GIT binary patch literal 931030 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%0D6f;axn+k1@4>tg=r7^uGnI^+vj) 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
    abgvNJsNP9VMx? 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%0Au_!rxF=iCf1scLKuc@Ee`4$vAaR|;+GHp4gO$_F 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�dp7&MW4n_lO=9-;O9#~lz}VA#?x-ez3B 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

    R$6^n|9gbfII(pnruu@NT8%)pW$e>; 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 literal 0 HcmV?d00001 diff --git a/numskull.cabal b/numskull.cabal index 721d77c..c50cc8d 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -18,12 +18,12 @@ library , NdArray , NdArrayException , DType - --, MatrixForm - --, Indexing - --, QuasiSlice - --, QuasiSlice.Quote - --, Typing - --, Serialisation + , MatrixForm + , Indexing + , QuasiSlice + , QuasiSlice.Quote + , Typing + , Serialisation -- other-modules: -- other-extensions: build-depends: base >=4.13.0.0 From 88033f016923135d0c684d3e4d2194263971b9fe Mon Sep 17 00:00:00 2001 From: Rowan-Mather <76477727+Rowan-Mather@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:24:50 +0100 Subject: [PATCH 78/90] Delete .vscode directory --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4e8191a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "git.mergeEditor": true -} \ No newline at end of file From 60964f095bc3554c7e7d3261aab2828fdf6f1cc6 Mon Sep 17 00:00:00 2001 From: Rowan-Mather <76477727+Rowan-Mather@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:34:30 +0100 Subject: [PATCH 79/90] Create temp --- docs/temp | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/temp diff --git a/docs/temp b/docs/temp new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/temp @@ -0,0 +1 @@ + From a67db1da3e7e107539efb2bba619a7769a8048ed Mon Sep 17 00:00:00 2001 From: Rowan-Mather <76477727+Rowan-Mather@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:34:51 +0100 Subject: [PATCH 80/90] Add files via upload --- docs/Numskull.html | 188 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/Numskull.html diff --git a/docs/Numskull.html b/docs/Numskull.html new file mode 100644 index 0000000..d7b8d54 --- /dev/null +++ b/docs/Numskull.html @@ -0,0 +1,188 @@ + +Numskull

    Safe HaskellSafe-Inferred
    LanguageHaskell2010

    Numskull

    Synopsis

    Metadata

    class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a #

    All types storable within an NdArray must implement DType. + This defines some basic properties, mathematical operations and standards for conversion.

    Instances

    Instances details
    DType Int32 # 
    Instance details

    Defined in DType

    DType Int64 # 
    Instance details

    Defined in DType

    DType Bool # 
    Instance details

    Defined in DType

    DType Char # 
    Instance details

    Defined in DType

    DType Double # 
    Instance details

    Defined in DType

    DType Float # 
    Instance details

    Defined in DType

    DType Int # 
    Instance details

    Defined in DType

    Methods

    addId :: Int #

    multId :: Int #

    add :: Int -> Int -> Int #

    subtract :: Int -> Int -> Int #

    multiply :: Int -> Int -> Int #

    divide :: Int -> Int -> Int #

    div :: Int -> Int -> Int #

    power :: Int -> Double -> Double #

    pow :: Int -> Int -> Int #

    log :: Int -> Int -> Int #

    mod :: Int -> Int -> Int #

    abs :: Int -> Int #

    signum :: Int -> Int #

    ceil :: Int -> Int #

    floor :: Int -> Int #

    sin :: Int -> Int #

    cos :: Int -> Int #

    tan :: Int -> Int #

    invert :: Int -> Int #

    shiftleft :: Int -> Int #

    shiftright :: Int -> Int #

    dtypeToRational :: Int -> Rational #

    rationalToDtype :: Rational -> Int #

    size :: [Integer] -> Int #

    Gets the total number of elements in a given array shape. + >>> size [2,3] + 6

    shape :: NdArray -> [Integer] #

    Returns the shape list of an array.

    getVector :: forall a. DType a => NdArray -> Vector a #

    Gets the vector of an array. Requires a type specification to output safely.

    ndType :: NdArray -> String #

    Gets the TypeRep String representation of the NdArray elements

    checkNdType :: forall a b. (DType a, DType b) => NdArray -> TypeRep a -> Maybe (a :~~: b) #

    Compares the type of the array elements to the given TypeRep.

    isEmpty :: NdArray -> Bool #

    Checks if the undelying vector has any elements.

    Creation

    data NdArray #

    The core of this module. NdArrays can be of any DType a and size/shape (list of dimensions) + These are hidden by the type.

    Instances

    Instances details
    Num NdArray # 
    Instance details

    Defined in Numskull

    Show NdArray #

    By default arrays are printed flat with the shape as metadata. + For a tidier representation, use printArray.

    Instance details

    Defined in NdArray

    Eq NdArray # 
    Instance details

    Defined in Numskull

    Ord NdArray # 
    Instance details

    Defined in Numskull

    fromList :: DType a => [Integer] -> [a] -> NdArray #

    Creates an NdArray from a given shape and list. The number of elements must match. + >>> printArray $ fromList [2,2] [1,2,3,4::Int] + 1 2 + 3 4

    fromListFlat :: DType a => [a] -> NdArray #

    Creates a 1xn NdArray from a list. + >>> printArray $ fromListFlat [1,2,3,4::Int] + 1 2 3 4

    data TreeMatrix a #

    This type is specifically for pretty explicit definitions of NdArrays. +The A constructor is for Array - a set of values and B is the value. +-- Example 2x3x2 +l :: TreeMatrix Int +l = A [A [A [B 1, B 2], + A [B 3, B 4], + A [B 5, B 6]],

    A [A [B 7, B 8], + A [B 9, B 10], + A [B 11, B 12]]]

    Constructors

    B a 
    A [TreeMatrix a] 

    fromMatrix :: DType a => TreeMatrix a -> NdArray #

    Creates an NdArray from an explicitly given matrix such as the example 2x3.

    fromVector :: DType a => [Integer] -> Vector a -> Maybe NdArray #

    The safe standard constructor. Returns Nothing if the + shape does not match the given vector length.

    singleton :: DType a => a -> NdArray #

    Creates a 1x1 matrix + >>> printArray $ singleton (3::Int) + 3

    arange :: (Enum a, DType a) => a -> a -> NdArray #

    Creates a flat array over the specified range.

    zeros :: forall a. DType a => TypeRep a -> [Integer] -> NdArray #

    Creates an array of the given shape of the identity element for the given type.

    squareArr :: forall a. DType a => [a] -> NdArray #

    Creates the smallest possible square matrix from the given list, +padding out any required space with the identity element for the DType

    Modification

    update :: forall a. DType a => NdArray -> [Integer] -> a -> NdArray #

    General Mapping, Folding & Zipping

    foldrA :: forall a b. DType a => (a -> b -> b) -> b -> NdArray -> b #

    Near identical to a standard foldr instance, expect NdArrays do not have an explicit type. +Folds in row-major order.

    mapA :: forall a. forall b. (DType a, DType b) => (a -> b) -> NdArray -> NdArray #

    Near identical to a standard map implementation in row-major order.

    mapTransform :: (forall a. DType a => a -> a) -> NdArray -> NdArray #

    Maps functions which return the same type.

    pointwiseZip :: (forall t. DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray #

    The generic function for operating on two matching DType arrays with the same shape + in an element-wise/pointwise way. Errors if mismatching + >>> x = fromList [2,2] [1,2,3,4 :: Int] + >>> y = fromList [2,2] [5,2,2,2 :: Int] + >>> printArray $ pointwiseZip (DType.multiply) x y + 5 4 + 6 8

    pointwiseBool :: (forall t. DType t => t -> t -> Bool) -> NdArray -> NdArray -> NdArray #

    A slightly specialised version of pointwise zip intended for comparative functions.

    zipArrayWith :: forall a b c. (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray #

    Completely generic zip on two NdArrays. If the shapes mismatch, they are truncated as with + standard zips. Function inputs must match the DTypes.

    Summaries

    origin :: forall a. DType a => NdArray -> a #

    Returns the element at the 0th position of the array.

    maxElem :: forall a. DType a => NdArray -> a #

    Returns the largest element.

    minElem :: forall a. DType a => NdArray -> a #

    Returns the smallest element.

    Mathematical constant

    scale :: forall a. DType a => a -> NdArray -> NdArray #

    Multiplies all elements by a scalar.

    absA :: NdArray -> NdArray #

    Takes the absolute value of all elements.

    signumA :: NdArray -> NdArray #

    Replaces all elements by their signum. + >>> printArray $ signumA (fromList [5] [-50, -25, 0, 1, 10::Int]) + -1 -1 0 1 1

    ceilA :: NdArray -> NdArray #

    Mathematical ceiling of each element (preserving DType).

    floorA :: NdArray -> NdArray #

    Mathematical floor of each element (preserving DType).

    sinA :: NdArray -> NdArray #

    Sine of each element (preserving DType).

    cosA :: NdArray -> NdArray #

    Cosine of each element (preserving DType).

    tanA :: NdArray -> NdArray #

    Tangent of each element (preserving DType).

    invertA :: NdArray -> NdArray #

    Either elementwise NOT or NEG depending on the DType.

    shiftleftA :: NdArray -> NdArray #

    Multiply each element by 2.

    shiftrightA :: NdArray -> NdArray #

    Divide each element by 2.

    Mathematical pointwise

    elemDivide :: NdArray -> NdArray -> NdArray #

    Pointwise division

    elemDiv :: NdArray -> NdArray -> NdArray #

    Pointwise integer division. Will return an NdArray of type Int.

    elemPow :: NdArray -> NdArray -> NdArray #

    Pointwise exponentiation (preserving DType)

    elemPower :: NdArray -> NdArray -> NdArray #

    Pointwise exponentiation which forces precision. + Takes some NdArray of bases, an array of Double exponents and returns an array of Doubles.

    sum :: [NdArray] -> NdArray #

    Takes the pointwise sum over all the given NdArrays. If they are different shapes, + the smaller dimensions are padded out with the identity element. + The sum of the empty list is the singleton 0.

    mean :: [NdArray] -> NdArray #

    Finds the mean pointwise over the list of arrays. Smaller arrays are padded out with + the identity element.

    Bounds

    clip :: forall a. DType a => Maybe a -> Maybe a -> NdArray -> NdArray #

    Constrains all elements of the array to the range specified by [mini, maxi]. + If they are given as Nothing, the range is infinite in that direction. + NB: must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd

    Type Conversions

    convertDTypeTo :: forall a. DType a => TypeRep a -> NdArray -> NdArray #

    Converting between the standard dtypes and changing the shapes of arrays. +NB the difference between size and shape. The shape is an Integer list +describing the width of each dimension. Size refers to the total number of +elements in the array, i.e. the product of the shape.

    Converts an NdArray of one type to any other with a DType instance.

    matchDType :: NdArray -> NdArray -> NdArray #

    Converts the second NdArray to be the same DType as the first.

    Size Conversions

    resize :: Integer -> NdArray -> NdArray #

    Truncate or pad the NdArray to match the new given size. +The shape will be collapsed to 1xn.

    Shape Conversions/Manipulations

    reshape :: [Integer] -> NdArray -> Maybe NdArray #

    Shape-shift one array to another of the same size (Nothing otherwise). + >>> x = fromList [2,3] [1,2,3,4,5,6 :: Int] + >>> printArray x + 1 2 + 3 4 + 5 6 + >>> printArray $ fromJust $ reshape [3,2] x + 1 2 3 + 4 5 6

    padShape :: [Integer] -> NdArray -> NdArray #

    Adds zero-rows to an array. Will error if you map to a smaller shape. + >>> x = fromList [2,2] [1,2,3,4 :: Int] + >>> printArray $ padShape [4,3] x + 1 2 0 0 + 3 4 0 0 + 0 0 0 0

    constrainShape :: [Integer] -> NdArray -> NdArray #

    Truncates the array to be no larger than the specified dimensions.

    broadcast :: (NdArray, NdArray) -> Maybe (NdArray, NdArray) #

    Takes a pair of NdArrays and attempts to copy slices so that they are size matched. + Arrays are broadcastable if they either match in corresponding dimensions or one is + of dimension size 1 e.g. [2,5,1] and [2,1,6]. Missing dimensions are padded with 1s + e.g. [1,2,3] and [3] are broadcastable.

    concatAlong :: Int -> [NdArray] -> Maybe NdArray #

    Concatenate a list of tensors into a single tensor. All input tensors must have the + same shape, except for the dimension size of the axis to concatenate on. + Returns Nothing if the arrays are not all of the same type or matching shapes.

    gather :: NdArray -> [Integer] -> Integer -> NdArray #

    Takes an array, set of sub-indices and axis and repeatedly takes slices + of the array restricted to that index along the specified axis. + The slices are then concatenated into the final array.

    Matrix Manipulation

    swapRows :: Integer -> Integer -> NdArray -> NdArray #

    Switches the rows at the two given indices over. +NB: designed for 2x2 matrices so will only make swaps in the front matrix of a tensor.

    diagonal :: NdArray -> NdArray #

    Gets the flat array of the leading diagonal of the front matrix of the tensor.

    transpose :: NdArray -> NdArray #

    Reverses the order of axes and switches the elements accordingly.

    transposePerm :: [Int] -> NdArray -> NdArray #

    Transposes the axes of an array according to the given permutation (e.g. [2,0,1])

    Matrix Multiplication

    dot :: forall a. DType a => NdArray -> NdArray -> a #

    Dot product over matricies of the same shape.

    matMul :: NdArray -> NdArray -> NdArray #

    Standard matrix multiplication following NumPy conventions. + 1D arrays have the extra dimension pre/appended + 2D arrays are multiplied as expected + ND-arrays are broadcast to match each other where possible and treated as stacks of nxm/pxq arrays.

    upperTriangle :: NdArray -> NdArray #

    Converts a nxn matrix to upper triangle form. O(n^3).

    determinant :: forall a. DType a => NdArray -> [a] #

    Finds the determinant(s) of a tensor. Over matrices of more than two dimensions +each 2D matrix's determinant is individually calculated and concatenated together (as in numpy: +https:/numpy.orgdocstablereferencegeneratednumpy.linalg.det.html ). +If the matrix is non-square it is assumed to be padded out and will have determinant of 0

    determinant2D :: forall a. DType a => NdArray -> a #

    Calculates the determinant of a 2D matrix using LU decomposition as described in the +below paper. O(n^3). +https:/informatika.stei.itb.ac.id~rinaldi.munirMatdis2016-2017Makalah2016Makalah-Matdis-2016-051.pdf

    gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray #

    General matrix multiplication. Calculates alpha*AB + beta*C with the option +to transpose A and B first. +Takes A, B, C, A transpose?, B transpose?, alpha, beta +Returns nothing if the matrix types/sizes do not match. +Will attempt to broadcast the shape of C and convert the types of alpha & beta.

    For more information see: +https:/en.wikipedia.orgwiki/Basic_Linear_Algebra_Subprograms#Level_3 +NB: if the matrices are integers the scalars will also become integers so you should convert the matrices first

    Indexing

    collapseInd :: [Integer] -> [Integer] -> Integer #

    Converts a shape and multi-index to a 1D index.

    expandInd :: [Integer] -> Integer -> [Integer] #

    Converts a shape and 1D index to a multi-index.

    map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer #

    Converts the multi-index for one shape to another

    validIndex :: NdArray -> [Integer] -> Bool #

    Checks an index does not exceed the shape.

    (#!) :: DType a => NdArray -> [Integer] -> a #

    Takes a multi-dimensional index and returns the value in the NdArray at that position. +Indicies can be negative, where -1 is the row in that dimension. +If an index exceeds the size of its dimension, a value will still be returned, the identity +value for the array e.g. 0. To avoid this use !?.

    (#?) :: forall a. DType a => NdArray -> [Integer] -> Maybe a #

    The safer version of #! which returns Nothing if an index exceeds the shape bounds.

    slice :: [(Integer, Integer)] -> NdArray -> NdArray #

    Takes a series of ranges corresponding to each dimension in the array and returns +the sub-array. Indicies are inclusive and can be negative.

    (/!) :: NdArray -> QuasiSlice -> NdArray #

    The concise operator for slicing. Instead of providing an IndexRange, + You may QuasiQuote a NumPy-like index e.g. myArray /! [q|5,2:6,:3|]. + Unspecified values in ranges denote the start/end.

    Pretty Printing

    printArray :: NdArray -> IO () #

    Prints out the pretty NdArray representation.

    prettyShowArray :: NdArray -> String #

    Converts an NdArray to its pretty representation. + Values along a row are separated whitespace. Along a column, newlines. + For higher dimensions, an additional newline is added to separate the nxm matrices.

    (=@=) :: (Typeable a, Typeable b) => a -> b -> Maybe (a :~~: b) #

    eqTypeRep synonym, returning Just HRefl in the case of type equality. + >>> case True =@= False of + >>> Just HRefl -> putStrLn "Two Booleans will match" + >>> Nothing -> putStrLn "Mismatching types" + Two Booleans will match

    loadNpy :: FilePath -> IO NdArray #

    Loads an NdArray from a .npy file

    Orphan instances

    \ No newline at end of file From e7be68efd5afbb3504dfe14e7f1f9ed05592a51e Mon Sep 17 00:00:00 2001 From: Rowan-Mather <76477727+Rowan-Mather@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:35:21 +0100 Subject: [PATCH 81/90] Delete temp --- docs/temp | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/temp diff --git a/docs/temp b/docs/temp deleted file mode 100644 index 8b13789..0000000 --- a/docs/temp +++ /dev/null @@ -1 +0,0 @@ - From 95151da4a652b3134e2768b70ddee054b15831d6 Mon Sep 17 00:00:00 2001 From: Rowan-Mather <76477727+Rowan-Mather@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:36:37 +0100 Subject: [PATCH 82/90] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09f1a78..1afa131 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ code into a jupyter notebook. 5) ./start.sh Note that the work in main is Numskull 1.0. -Numskull 2.0 can be found in the so-called branch! The second version is less well tested and complete, but should be more efficient since it makes use of strides. I didn't have time to integrate that into the Onnx backend, but it shouldn't be at all difficult to do so. +Numskull 2.0 can be found in the so-called branch! The second version is less well tested and complete, but should be more efficient since it makes use of strides. I didn't have time to integrate that into the Onnx backend, but it shouldn't be at all difficult to do so. There is an open pull request so it's easy to find. ## Development From 4b9d29ab47a73ac1f96c40c665a1aca754b4541a Mon Sep 17 00:00:00 2001 From: Rowan Date: Fri, 25 Aug 2023 11:24:22 +0100 Subject: [PATCH 83/90] s --- src/Indexing.hs | 14 +++++++------- src/Numskull.hs | 6 +++--- src/QuasiSlice.hs | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Indexing.hs b/src/Indexing.hs index 2597c06..571d8eb 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -37,8 +37,8 @@ shape of the array, [sx,sy,sz,...], as follows: -- * INDEXING -- | Generates the list of all multi-dimensional indices for a given shape -generateIndicies :: [Integer] -> [[Integer]] -generateIndicies = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] +generateIndices :: [Integer] -> [[Integer]] +generateIndices = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] {- | Generates two maps to convert between the single dimension index of the underlying vector and the multi-dimensional index of the NdArray and back, @@ -47,7 +47,7 @@ given the NdArray shape. mapIndices :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) mapIndices sh = (M.fromList oneDkey, M.fromList twoDkey) where - twoDinds = generateIndicies sh + twoDinds = generateIndices sh oneDkey = zip [0..] twoDinds twoDkey = zip twoDinds [0..] @@ -85,7 +85,7 @@ validIndex (NdArray s _) i = (length i == length s) && and (zipWith lessAbs i s) where lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) {- | Takes a multi-dimensional index and returns the value in the NdArray at that position. -Indicies can be negative, where -1 is the row in that dimension. +Indices can be negative, where -1 is the row in that dimension. If an index exceeds the size of its dimension, a value will still be returned, the identity value for the array e.g. 0. To avoid this use !?. -} @@ -119,7 +119,7 @@ value for the array e.g. 0. To avoid this use !?. Nothing -> Nothing else Nothing --- Converts negative indicies to their positive equivalents, counting back +-- Converts negative indices to their positive equivalents, counting back -- from the end of the array (i.e. -1 is the last element). positiveInd :: (Ord a, Num a) => a -> a -> a positiveInd s i = if i < 0 then s+i else i @@ -129,7 +129,7 @@ positiveInd s i = if i < 0 then s+i else i positiveRanges :: [Integer] -> [(Integer, Integer)] -> [(Integer, Integer)] positiveRanges = zipWith (\s (x,y) -> (positiveInd s x, if y < 0 then s+y else y-1)) --- Converts an IndexRange to a range of indicies in the standard pair form. +-- Converts an IndexRange to a range of indices in the standard pair form. forceRange :: Integer -> IndexRange -> (Integer, Integer) forceRange sh (I i) = (positiveInd sh i, positiveInd sh i) forceRange sh (R s t) = (positiveInd sh s, if t < 0 then positiveInd sh t else t-1) @@ -146,7 +146,7 @@ forceRange sh (R s t) = (positiveInd sh s, if t < 0 then positiveInd sh t else t (#!+) (NdArray s v) irs = slice (zipWith forceRange s irs) (NdArray s v) {- | Takes a series of ranges corresponding to each dimension in the array and returns -the sub-array. Indicies are inclusive and can be negative. -} +the sub-array. Indices are inclusive and can be negative. -} slice :: [(Integer, Integer)] -> NdArray -> NdArray slice sl (NdArray s v) = let diff --git a/src/Numskull.hs b/src/Numskull.hs index 581f545..a2b1ee9 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -829,7 +829,7 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -- * Multiplication --- | Dot product over matricies of the same shape. +-- | Dot product over matrices of the same shape. dot :: forall a. DType a => NdArray -> NdArray -> a dot (NdArray s v) nd2 = foldrA DType.add (identityElem v <-@ typeRep @a) ((NdArray s v)*nd2) @@ -1012,9 +1012,9 @@ https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/M determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = case shape nd of - -- 2x2 matricies are calculated quickly with the standard ad-bc + -- 2x2 matrices are calculated quickly with the standard ad-bc [2,2] -> determinant2x2 nd :: a - -- nxn matricies are row-swapped to find an arrangement with no zeros/identity elements + -- nxn matrices are row-swapped to find an arrangement with no zeros/identity elements -- in the leading diagonal (pivots) then put into upper triangle form [c,r] | c == r && not (zeroRow nd) -> case swapRowsWith0Pivot nd of Just (NdArray s v) -> diff --git a/src/QuasiSlice.hs b/src/QuasiSlice.hs index 7cef9b8..e1683c6 100644 --- a/src/QuasiSlice.hs +++ b/src/QuasiSlice.hs @@ -12,7 +12,7 @@ import Text.ParserCombinators.Parsec import Data.Typeable import Data.Data --- | Type which allows you to provide only a single index or a range of indicies. +-- | Type which allows you to provide only a single index or a range of indices. data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) -- QuasiQuoted slices are converted to this to be evaluated. @@ -48,8 +48,8 @@ symbol name = lexeme (string name) comma = do{ symbol ","; return $ CommaEx } -indiciesExpr :: CharParser st QuasiSlice -indiciesExpr = sliceIndex `chainl1` comma +indicesExpr :: CharParser st QuasiSlice +indicesExpr = sliceIndex `chainl1` comma number :: CharParser st QuasiSlice number = do @@ -96,6 +96,6 @@ parseSlice (file, line, col) s = (flip setSourceColumn) col $ pos spaces - e <- indiciesExpr + e <- indicesExpr eof return e \ No newline at end of file From 056855a0b125ffd725f8ae4e91e284d13e31b749 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Tue, 26 Sep 2023 23:29:01 +0100 Subject: [PATCH 84/90] Remove TemplateHaskell extension It was causing $length to be treated as a splice --- src/Numskull.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Numskull.hs b/src/Numskull.hs index a2b1ee9..4dcfbe7 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -4,7 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TemplateHaskellQuotes #-} {-# LANGUAGE QuasiQuotes #-} module Numskull ( From ad3099a943a515b438a9cedbb266f72280680115 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 27 Sep 2023 00:10:59 +0100 Subject: [PATCH 85/90] Add NFData instance --- numskull.cabal | 1 + src/DType.hs | 3 ++- src/NdArray.hs | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/numskull.cabal b/numskull.cabal index c50cc8d..57c0a53 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -32,6 +32,7 @@ library , containers , parsec , template-haskell + , deepseq hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall diff --git a/src/DType.hs b/src/DType.hs index 848c8ba..cde5717 100644 --- a/src/DType.hs +++ b/src/DType.hs @@ -8,10 +8,11 @@ import Type.Reflection import GHC.Float (float2Double) import Data.Int import Data.Char +import Control.DeepSeq (NFData) -- | All types storable within an NdArray must implement DType. -- This defines some basic properties, mathematical operations and standards for conversion. -class (Typeable a, Storable a, Show a, Eq a, Ord a) => DType a where +class (Typeable a, Storable a, Show a, Eq a, Ord a, NFData a) => DType a where -- | Additive identity addId :: a -- | Multiplicative identity diff --git a/src/NdArray.hs b/src/NdArray.hs index 8c13d89..21bd39a 100644 --- a/src/NdArray.hs +++ b/src/NdArray.hs @@ -4,6 +4,7 @@ module NdArray where import DType import Data.Vector.Storable +import Control.DeepSeq (NFData (rnf), deepseq) -- * NdArray -- | The core of this module. NdArrays can be of any DType a and size/shape (list of dimensions) @@ -14,4 +15,7 @@ data NdArray where -- | By default arrays are printed flat with the shape as metadata. -- For a tidier representation, use printArray. instance Show NdArray where - show (NdArray s v) = "{elements: " <> show v <> ", shape: " <> show s <> "}" \ No newline at end of file + show (NdArray s v) = "{elements: " <> show v <> ", shape: " <> show s <> "}" + +instance NFData NdArray where + rnf (NdArray s v) = s `deepseq` v `deepseq` () From 9d435686ad748d8932be6f4517a0f26edafbdbfe Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Tue, 3 Oct 2023 23:40:03 +0100 Subject: [PATCH 86/90] Add tests to cabal file --- numskull.cabal | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/numskull.cabal b/numskull.cabal index 57c0a53..6dadeb6 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -35,4 +35,14 @@ library , deepseq hs-source-dirs: src default-language: Haskell2010 - ghc-options: -Wall + ghc-options: -O2 -Wall + +test-suite tests + type: exitcode-stdio-1.0 + main-is: Test.hs + build-depends: base + , numskull + , QuickCheck + , hspec + hs-source-dirs: test + ghc-options: -O2 -Wall From 9895df0b1fa09690c61efa87c97fab430fa4477f Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Tue, 3 Oct 2023 23:40:48 +0100 Subject: [PATCH 87/90] Add upper bound on base dep --- numskull.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numskull.cabal b/numskull.cabal index 6dadeb6..5b96366 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -26,7 +26,7 @@ library , Serialisation -- other-modules: -- other-extensions: - build-depends: base >=4.13.0.0 + build-depends: base >=4.13.0.0 && <5 , vector , split , containers @@ -40,7 +40,7 @@ library test-suite tests type: exitcode-stdio-1.0 main-is: Test.hs - build-depends: base + build-depends: base >=4.13.0.0 && <5 , numskull , QuickCheck , hspec From fb9711e1ab47acf47b4bc93e64364a522c778a40 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Tue, 3 Oct 2023 23:40:59 +0100 Subject: [PATCH 88/90] Remove invalid cabal --- numskull.cabal | 1 - 1 file changed, 1 deletion(-) diff --git a/numskull.cabal b/numskull.cabal index 5b96366..6899fa9 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -13,7 +13,6 @@ build-type: Simple -- extra-source-files: library - import library-deps exposed-modules: Numskull , NdArray , NdArrayException From 7db8be0e5d9695556dc8c322835d627ae6460871 Mon Sep 17 00:00:00 2001 From: Aiken Cairncross Date: Wed, 4 Oct 2023 13:49:58 +0100 Subject: [PATCH 89/90] Re-run cabal2nix --- numskull.nix | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/numskull.nix b/numskull.nix index 8f063b7..ad4a018 100644 --- a/numskull.nix +++ b/numskull.nix @@ -1,13 +1,13 @@ -{ mkDerivation, base, lib, split -, vector +{ mkDerivation, base, containers, deepseq, hspec, lib, parsec +, QuickCheck, split, template-haskell, vector }: mkDerivation { pname = "numskull"; version = "0.1.0.0"; src = ./.; - libraryHaskellDepends = [ base split vector ]; - testHaskellDepends = [ - base split vector + libraryHaskellDepends = [ + base containers deepseq parsec split template-haskell vector ]; + testHaskellDepends = [ base hspec QuickCheck ]; license = lib.licenses.mit; -} \ No newline at end of file +} From 7b83bb613f52ca33bb566fa0ceb84efef4239662 Mon Sep 17 00:00:00 2001 From: Rowan Date: Thu, 10 Aug 2023 17:08:56 +0100 Subject: [PATCH 90/90] Numskull 2.0 starting stride redesign updating lots of functions updating lots of functions pretty printing & broadcast config pointwise stride & some matmul work transposition some determinants fixing more determinant ops concatenation bug fixing slices & compiles single indexing s --- numskull.cabal | 2 +- src/Indexing.hs | 171 +++++--- src/MatrixForm.hs | 29 +- src/NdArray.hs | 9 +- src/NdArrayException.hs | 23 +- src/Numskull.hs | 903 +++++++++++++++++++++++++--------------- src/Serialisation.hs | 3 +- src/SliceTest.hs | 32 ++ 8 files changed, 742 insertions(+), 430 deletions(-) create mode 100644 src/SliceTest.hs diff --git a/numskull.cabal b/numskull.cabal index 6899fa9..5a3f306 100644 --- a/numskull.cabal +++ b/numskull.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: numskull -version: 0.1.0.0 +version: 0.2.0.0 -- synopsis: -- description: license: MIT diff --git a/src/Indexing.hs b/src/Indexing.hs index 571d8eb..a888573 100644 --- a/src/Indexing.hs +++ b/src/Indexing.hs @@ -1,26 +1,47 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE TemplateHaskell #-} module Indexing where + import Control.Exception import qualified Data.Vector.Storable as V import Data.Vector.Storable (Vector) import qualified Data.Map as M import Type.Reflection -import Text.ParserCombinators.Parsec import NdArray import Typing import qualified DType import DType (DType) import NdArrayException -import QuasiSlice -import QuasiSlice.Quote +preciseDiv x y = fromIntegral @Int @Float x / fromIntegral @Int @Float y + +-- Takes the shape assuming you want to move directly between dimensions. +defStride :: Vector Int -> Vector Int +defStride sh = V.scanr' (*) 1 $ V.drop 1 sh + +stride :: NdArray -> NdArray +stride (NdArray sh st v) = + let + -- shape + dimAcc = V.scanr' (*) 1 sh + --newshape = V.map (i -> ((dim' V.!(i+1)) * (sh V.!i -1) +1) / st V.! i) (enumFromN 0 (V.length sh)) + newshape = + V.map (\i -> + ceiling $ preciseDiv (1 + (dimAcc V.! (i+1)) * (sh V.!i -1)) (st V.! i) :: Int) + (V.enumFromN 0 (V.length sh)) + --V.zipWith (\d s -> ceiling (preciseDiv d s) :: Int) [0..] st + -- stride + singleStride = defStride newshape + -- vector + grab i = V.and $ V.zipWith (\t h -> i `mod` t < h) st (V.drop 1 dimAcc) + newV = V.force (V.ifilter (\i _ -> grab i) v) + in + if V.all (==1) st then (NdArray sh st v) + else NdArray newshape singleStride newV {- | Arrays are stored as vectors with a shape. Since vectors only have one dimension, we convert between the vector index, i, and multi-dimension index, [x,y,z,...], using the @@ -37,14 +58,14 @@ shape of the array, [sx,sy,sz,...], as follows: -- * INDEXING -- | Generates the list of all multi-dimensional indices for a given shape -generateIndices :: [Integer] -> [[Integer]] +generateIndices :: [Int] -> [[Int]] generateIndices = foldr (\x xs -> [ i:t | i <- [0..(x-1)], t <- xs]) [[]] {- | Generates two maps to convert between the single dimension index of the underlying vector and the multi-dimensional index of the NdArray and back, given the NdArray shape. -} -mapIndices :: [Integer] -> (M.Map Int [Integer], M.Map [Integer] Int) +mapIndices :: [Int] -> (M.Map Int [Int], M.Map [Int] Int) mapIndices sh = (M.fromList oneDkey, M.fromList twoDkey) where twoDinds = generateIndices sh @@ -52,9 +73,10 @@ mapIndices sh = (M.fromList oneDkey, M.fromList twoDkey) twoDkey = zip twoDinds [0..] -- Indexes a vector with an NdArray multi-index using a mapping (unsafe). -vecInd :: forall a . DType a => M.Map [Integer] Int -> Vector a -> [Integer] -> a -vecInd mapp v i = v V.! (mapp M.! i) +--vecInd :: forall a . DType a => M.Map [Int] Int -> Vector a -> [Int] -> a- +--vecInd mapp v i = v V.! (mapp M.! i) +{- -- | Converts a shape and multi-index to a 1D index. collapseInd :: [Integer] -> [Integer] -> Integer collapseInd sh indices = collapseRun (reverse sh) (reverse indices) 1 @@ -74,15 +96,43 @@ expandRun :: [Integer] -> Integer -> Integer -> [Integer] expandRun [] _ _ = [] expandRun (s:ss) i runSize = x : expandRun ss i (s*runSize) where x = (i `div` runSize) `mod` s +-} +vGet :: DType a => Vector a -> Vector Int -> [Int] -> a +vGet v t is = v V.! (collapseInd t $ V.fromList is) + +collapseInd :: Vector Int -> Vector Int -> Int +collapseInd st ind = V.sum $ V.zipWith (*) st ind + +expandInd :: Vector Int -> Int -> Vector Int +expandInd st ind = + let st' = V.toList st + in V.fromList $ expandRun st' ind + +expandRun :: [Int] -> Int -> [Int] +expandRun [] _ = [] +expandRun (s:sts) x = + if s == 0 then (0 : expandRun sts x) + else x `div` s : expandRun sts (x `mod` s) + {- + let st' = V.map (/= 0) st + V.zipwith (/) (V.drop 1 $ V.scanl mod ind st) st + +scanl :: (b -> a -> b) -> b -> [a] -> [b] +scanl f z xs = foldr go (const []) xs z + where + go x continue acc = let next = f acc x in next : continue next +-} -- | Converts the multi-index for one shape to another -map1DIndex :: [Integer] -> [Integer] -> Integer -> Integer -map1DIndex s r i = collapseInd r (expandInd s i) +map1DIndex :: Vector Int -> Vector Int -> Int -> Int +map1DIndex t d i = collapseInd d (expandInd t i) -- | Checks an index does not exceed the shape. -validIndex :: NdArray -> [Integer] -> Bool -validIndex (NdArray s _) i = (length i == length s) && and (zipWith lessAbs i s) - where lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) +validIndex :: NdArray -> [Int] -> Bool +validIndex (NdArray _ sh _) i = (length i == length s) && and (zipWith lessAbs i s) + where + s = V.toList sh + lessAbs x y = (0 <= x && x < y) || (0 < -x && -x <= y) {- | Takes a multi-dimensional index and returns the value in the NdArray at that position. Indices can be negative, where -1 is the row in that dimension. @@ -94,8 +144,8 @@ value for the array e.g. 0. To avoid this use !?. -- 4 -- >>> m #! [50] :: Int -- 0 -(#!) :: DType a => NdArray -> [Integer] -> a -(NdArray s v) #! i = case NdArray s v #? i of +(#!) :: DType a => NdArray -> [Int] -> a +(NdArray st sh v) #! i = case NdArray sh sh v !? i of Just val -> val Nothing -> DType.addId :: DType a => a @@ -105,70 +155,60 @@ value for the array e.g. 0. To avoid this use !?. -- Just 4 -- >>> m !? [50] :: Maybe Int -- Nothing -(#?) :: forall a . DType a => NdArray -> [Integer] -> Maybe a -(NdArray s v) #? i = +(!?) :: forall a . DType a => NdArray -> [Int] -> Maybe a +(NdArray st sh v) !? i = let -- Converts any negative indices to their equivalent positives - positives = zipWith positiveInd s i - flatInd = fromIntegral $ collapseInd s positives :: Int + positives = V.zipWith positiveInd sh (V.fromList i) + flatInd = fromIntegral $ collapseInd st positives :: Int in -- The type comparison should always hold - if validIndex (NdArray s v) i then + if validIndex (NdArray st sh v) i then case ty v `eqTypeRep` typeRep @(Vector a) of Just HRefl -> Just (v V.! flatInd) :: Maybe a -- Indexing the vector Nothing -> Nothing else Nothing --- Converts negative indices to their positive equivalents, counting back --- from the end of the array (i.e. -1 is the last element). -positiveInd :: (Ord a, Num a) => a -> a -> a -positiveInd s i = if i < 0 then s+i else i - -- * SLICING +{- +-- | Type which allows you to provide only a single index or a range of indices. +data IndexRange = I Integer | R Integer Integer deriving (Show, Eq) -positiveRanges :: [Integer] -> [(Integer, Integer)] -> [(Integer, Integer)] -positiveRanges = zipWith (\s (x,y) -> (positiveInd s x, if y < 0 then s+y else y-1)) - --- Converts an IndexRange to a range of indices in the standard pair form. -forceRange :: Integer -> IndexRange -> (Integer, Integer) -forceRange sh (I i) = (positiveInd sh i, positiveInd sh i) -forceRange sh (R s t) = (positiveInd sh s, if t < 0 then positiveInd sh t else t-1) - --- | The concise operator for slicing. Instead of providing an IndexRange, --- You may QuasiQuote a NumPy-like index e.g. myArray /! [q|5,2:6,:3|]. --- Unspecified values in ranges denote the start/end. -(/!) :: NdArray -> QuasiSlice -> NdArray -(/!) nd sl = nd #!+ (evalSlice sl) - --- Integrated indexing and slicing. For each dimension you can provide either a single value +-- | Integrated indexing and slicing. For each dimension you can provide either a single value -- or a range of values where a slice will be taken. (#!+) :: NdArray -> [IndexRange] -> NdArray -(#!+) (NdArray s v) irs = slice (zipWith forceRange s irs) (NdArray s v) +(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) + where (m,_) = mapIndices sh + +-- Converts an IndexRange to a range of indices in the standard pair form. +forceRange :: IndexRange -> (Integer, Integer) +forceRange (I i) = (i,i) +forceRange (R s t) = (s,t) +-} +-- Converts negative indices to their positive equivalents, counting back +-- from the end of the array (i.e. -1 is the last element). +positiveInd :: (Ord a, Num a) => a -> a -> a +positiveInd s i = if i < 0 then s+i else i {- | Takes a series of ranges corresponding to each dimension in the array and returns the sub-array. Indices are inclusive and can be negative. -} -slice :: [(Integer, Integer)] -> NdArray -> NdArray -slice sl (NdArray s v) = - let - pad = sl ++ replicate (length s - length sl) (0,-1) - sl' = zipWith (\(x,y) sk -> (positiveInd sk x, positiveInd sk y)) pad s - inds = sequence $ map (\(x, y) -> [x..y]) sl' - flatinds = V.fromList $ map (fromInteger @Int . collapseInd s) inds - newshape = map (\(x,y) -> y-x+1) sl' - in NdArray newshape $ V.map (v V.!) flatinds - -{- -slicing with maps ---slice :: [(Integer, Integer)] -> NdArray -> NdArray +slice :: [(Int, Int)] -> NdArray -> NdArray +slice sl (NdArray sh st v) = + let -- todo: -ve indices + ranges = zipWith (\(i,j) t -> [t*i, t*(i+1) .. t*j]) sl (V.toList st) + indices = V.fromList $ (map sum . sequence) ranges + sh' = V.fromList $ map (\(i,j) -> j-i+1) sl + v' = V.map (v V.!) indices + in + NdArray sh' (defStride sh') v' --slice ss (NdArray sh v) = sliceWithMap m 0 ss (NdArray sh v) -- where (m,_) = mapIndices sh +-- https://rosettacode.org/wiki/Cartesian_product_of_two_or_more_lists#Haskell ---(#!+) (NdArray sh v) irs = sliceWithMap m 0 (map forceRange irs) (NdArray sh v) --- where (m,_) = mapIndices sh - --- Equivalent slicing operator. ---(!/) :: NdArray -> [(Integer, Integer)] -> NdArray ---(!/) nd ss = slice ss nd +{- +-- | Equivalent slicing operator. +(!/) :: NdArray -> [(Integer, Integer)] -> NdArray +(!/) nd ss = slice ss nd -- Takes a slice on an NdArray given the mapping from the vector index to NdArray index. -- Iterates through each dimension of the slice one at a time. @@ -178,20 +218,21 @@ sliceWithMap _ d _ (NdArray sh v) | d >= length sh = NdArray sh v sliceWithMap m d (s : ss) (NdArray sh v) = sliceWithMap m (d+1) ss $ sliceDim s d m (NdArray sh v) --- Takes a slice of an NdArray at a particular dimension, no -ve indices. +-- Takes a slice of an NdArray at a particular dimension. sliceDim :: (Integer, Integer) -> Int -> M.Map Int [Integer] -> NdArray -> NdArray sliceDim (x,y) d m (NdArray sh v) = if d >= length sh then throw (ExceededShape (fromIntegral d) sh) else NdArray - (if y < x then [] else shrinkNth d (y-x+1) sh) + (if y' < x' then [] else shrinkNth d (y'-x'+1) sh) (V.ifilter (\i _ -> let dimInd = (m M.! i) !! d - in x <= dimInd && dimInd <= y) + in x' <= dimInd && dimInd <= y') v ) where dimSize = sh !! d + (x', y') = (positiveInd dimSize x, positiveInd dimSize y) -- Replaces the nth value of an array if the newValue is smaller. -- https://stackoverflow.com/questions/5852722/replace-individual-list-elements-in-haskell @@ -200,4 +241,4 @@ shrinkNth _ _ [] = [] shrinkNth n newVal (x:xs) | n == 0 = if newVal < x then newVal:xs else x:xs | otherwise = x:shrinkNth (n-1) newVal xs --} \ No newline at end of file + -} \ No newline at end of file diff --git a/src/MatrixForm.hs b/src/MatrixForm.hs index ea5c182..29a718e 100644 --- a/src/MatrixForm.hs +++ b/src/MatrixForm.hs @@ -1,9 +1,11 @@ module MatrixForm where + import NdArray +import Indexing import Data.Tree import qualified Data.Vector.Storable as V - +{- -- * READING MATRICIES {- | This type is specifically for pretty explicit definitions of NdArrays. @@ -37,7 +39,7 @@ treeShape t = zipWith (\x y -> fromIntegral $ div x y ::Integer) (drop 1 levelLe -- Calculates the shape of the NdArray corresponding to the TreeMatrix. matrixShape :: TreeMatrix a -> [Integer] matrixShape = treeShape . matrixToTree - +-} -- * WRITING MATRICIES -- | Prints out the pretty NdArray representation. @@ -48,22 +50,25 @@ printArray nd = putStr $ prettyShowArray nd -- Values along a row are separated whitespace. Along a column, newlines. -- For higher dimensions, an additional newline is added to separate the nxm matrices. prettyShowArray :: NdArray -> String -prettyShowArray (NdArray s v) = conc <> "\n" - where - vl = map show (V.toList v) - largest = maximum $ map length vl - newlines = scanr1 (*) s - spaced = zipWith (\i x -> (i, padStringTo largest x)) [0..] vl - lined = addNewlines newlines spaced - conc = concatMap snd lined +prettyShowArray nd = + case stride nd of + (NdArray s _ v) -> + conc <> "\n" + where + vl = map show (V.toList v) + largest = maximum $ map length vl + newlines = scanr1 (*) (V.toList s) + spaced = zipWith (\i x -> (i, padStringTo largest x)) [0..] vl + lined = addNewlines newlines spaced + conc = concatMap snd lined -- Separates values along a row by whitespace. padStringTo :: Int -> String -> String padStringTo i s = replicate (i - length s) ' ' ++ s ++ " " -- Separates columns and higher dimensions by newlines. -addNewlines :: [Integer] -> [(Integer, String)] -> [(Integer, String)] +addNewlines :: [Int] -> [(Int, String)] -> [(Int, String)] addNewlines ls xs = foldr (\l -> map (\(i, x) -> if i /= 0 && i `mod` l == 0 then (i, "\n" ++ x) - else (i, x))) xs ls \ No newline at end of file + else (i, x))) xs ls diff --git a/src/NdArray.hs b/src/NdArray.hs index 21bd39a..da990b2 100644 --- a/src/NdArray.hs +++ b/src/NdArray.hs @@ -4,18 +4,15 @@ module NdArray where import DType import Data.Vector.Storable -import Control.DeepSeq (NFData (rnf), deepseq) -- * NdArray -- | The core of this module. NdArrays can be of any DType a and size/shape (list of dimensions) -- These are hidden by the type. data NdArray where - NdArray :: DType a => [Integer] -> Vector a -> NdArray + -- shape stride elements + NdArray :: DType a => Vector Int -> Vector Int -> Vector a -> NdArray -- | By default arrays are printed flat with the shape as metadata. -- For a tidier representation, use printArray. instance Show NdArray where - show (NdArray s v) = "{elements: " <> show v <> ", shape: " <> show s <> "}" - -instance NFData NdArray where - rnf (NdArray s v) = s `deepseq` v `deepseq` () + show (NdArray sh st v) = "{elements: " <> show v <> ", shape: " <> show sh <> ", stride: " <> show st <> "}" \ No newline at end of file diff --git a/src/NdArrayException.hs b/src/NdArrayException.hs index 071a407..e56dbbc 100644 --- a/src/NdArrayException.hs +++ b/src/NdArrayException.hs @@ -16,14 +16,15 @@ import NdArray data NdArrayException = DTypeMismatch NdArray NdArray String | ShapeMismatch NdArray NdArray String - | CreationSize Integer [Integer] + | CreationSize Int (Vector Int) | TypeMismatch String - | ExceededShape Integer [Integer] + | ExceededShape Int (Vector Int) + | NotBroadcastable NdArray NdArray String instance Exception NdArrayException instance Show NdArrayException where - show (DTypeMismatch (NdArray _ v) (NdArray _ u) extra) = + show (DTypeMismatch (NdArray _ _ v) (NdArray _ _ u) extra) = if extra == "" then "Cannot match NdArrays of type '" <> showType v <> "' and type '" <> showType u <> "'." @@ -31,13 +32,14 @@ instance Show NdArrayException where "Cannot perform " <> extra <> " on mismatching NdArrays of type '" <> showType v <> "' and type '" <> showType u <> "'." - show (ShapeMismatch (NdArray s _) (NdArray r _) extra) = + show (ShapeMismatch (NdArray s t _) (NdArray r d _) extra) = if extra == "" then - "Cannot match NdArrays of shape " <> show s <> - " and shape " <> show r <> "." + "Cannot match NdArrays of shape " <> show s <> " and stride " <> show t <> + ", and shape " <> show r <> "and stride " <> show d <> "." else - "Cannot perform " <> extra <> " on mismatching NdArrays of shape " <> show s <> - " and shape " <> show r <> "." + "Cannot perform " <> extra <> " on mismatching NdArrays of shape " <> + show s <> " and stride " <> show t <> + " and shape " <> show r <> "and stride " <> show d <> "." show (CreationSize sz sh) = "Cannot create array of size " <> show sz <> " and shape " <> show sh <> "." @@ -47,6 +49,9 @@ instance Show NdArrayException where show (ExceededShape dim sh) = "Cannot index into dimension " <> show dim <> "in NdArray of shape " <> show sh <> "." + show (NotBroadcastable (NdArray s _ _) (NdArray r _ _) str) = + "Cannot broadcast NdArrays of shape " <> show s <> "and shape" <> show r <> str <> "." + -- Returns the string type of vector elements. showType :: forall a . DType a => Vector a -> String -showType _ = show (typeRep @a) +showType _ = show (typeRep @a) \ No newline at end of file diff --git a/src/Numskull.hs b/src/Numskull.hs index 4dcfbe7..39b0dc7 100644 --- a/src/Numskull.hs +++ b/src/Numskull.hs @@ -4,11 +4,10 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE TemplateHaskellQuotes #-} -{-# LANGUAGE QuasiQuotes #-} module Numskull ( - -- * Metadata + {- + -- Metadata DType , size , shape @@ -17,11 +16,11 @@ module Numskull ( , checkNdType , isEmpty - -- * Creation + -- Creation , NdArray , fromList , fromListFlat - , TreeMatrix (..) + , TreeMatrix , fromMatrix , fromVector , singleton @@ -29,10 +28,7 @@ module Numskull ( , zeros , squareArr - -- * Modification - , update - - -- * General Mapping, Folding & Zipping + -- General mapping, folding & zipping , foldrA , mapA , mapTransform @@ -40,12 +36,12 @@ module Numskull ( , pointwiseBool , zipArrayWith - -- * Summaries + -- Summaries , origin , maxElem , minElem - -- * Mathematical constant + -- Mathematical constant , scale , absA , signumA @@ -58,7 +54,7 @@ module Numskull ( , shiftleftA , shiftrightA - -- * Mathematical pointwise + -- Mathematical pointwise , elemDivide , elemDiv , elemPow @@ -66,17 +62,17 @@ module Numskull ( , Numskull.sum , mean - -- * Bounds + -- Bounds , clip - -- * Type Conversions + -- Type Conversions , convertDTypeTo , matchDType - -- * Size Conversions + -- Size conversions , resize - -- * Shape Conversions/Manipulations + -- Shape conversions/manipulations , reshape , padShape , constrainShape @@ -84,13 +80,13 @@ module Numskull ( , concatAlong , gather - -- * Matrix Manipulation + -- Matrix manipulation , swapRows , diagonal , transpose , transposePerm - -- * Matrix Multiplication + --Matrix multiplication , dot , matMul , upperTriangle @@ -99,28 +95,29 @@ module Numskull ( , swapRowsWith0Pivot , gemm - -- * Indexing + -- Indexing + , IndexRange , collapseInd , expandInd , map1DIndex , validIndex , (#!) - , (#?) + , (!?) + , (#!+) , slice - , (/!) - , evalSlice - , q + , (!/) - -- * Pretty Printing + -- Pretty printing , printArray , prettyShowArray - -- Typing + -- typing , (=@=) - -- Numpy Serialisation + -- numpy serialisation , saveNpy , loadNpy +-} ) where import qualified DType @@ -131,14 +128,12 @@ import NdArray import NdArrayException import Serialisation import Typing -import QuasiSlice -import QuasiSlice.Quote import Control.Exception import Control.Monad (zipWithM) -import Data.List (elemIndex, intersect, sort, zipWith4) +import Data.List (elemIndex, intersect, sort, zipWith6) import qualified Data.Map as M -import Data.Maybe (fromJust) +import Data.Maybe (fromJust, isNothing) import Data.Vector.Storable (Vector) import qualified Data.Vector.Storable as V import Type.Reflection @@ -149,72 +144,124 @@ import Type.Reflection -- * Numeric & Comparative NdArray instances: -------------------------------------------------------------------------------- +--calculateNewshape :: Vector Int -> Vector Int -> Vector Int +--c-alculateNewshape sh st = V.generate (V.length sh) $ i -> +-- (sh V.! i + 1) / (st V.! i) + +--V.scanr' (*) 1 (V.drop 1 $ V.fromList [10,5,5::Int]) + + + +--force $ and also check for emptyness + +{- +grab :: Int -> Vector Int -> Vector Int -> Bool +grab i sh st = + let + dimAccSml = V.scanr' (*) 1 $ V.drop 1 sh + p = V.zipWith (\t h -> i `mod` t < h) st dimAccSml + in V.and p + + +stride :: NdArray -> Vector Int +stride (NdArray sh st v) = --force $ and also check for emptyness + let + dimAcc = V.scanr1' (*) sh + fl = fromIntegral @Int @Float + newshape = V.zipWith (\d s -> ceiling (fl d / fl s) ::Int) dimAcc st + coef = V.drop 1 dimAcc + in + newshape -} + {-V.generate (V.head dimAcc) $ i -> + let + x = 4 + V.zipWith + in + undefined + -} + +n1 = NdArray (V.fromList [2,2]) (V.fromList [2,1]) (V.fromList [1,2,3,4::Int]) +n2 = NdArray (V.fromList [3,3]) (V.fromList [3,1]) (V.fromList [1..9::Int]) +n3 = NdArray (V.fromList [4,4]) (V.fromList [4,2]) (V.fromList [1..16::Int]) +n4 = NdArray (V.fromList [4,2]) (V.fromList [2,1]) (V.fromList [1,3,5,7,9,11,13,15::Int]) +n5 = NdArray (V.fromList [2]) (V.fromList [1]) (V.fromList [1,2::Int]) instance Eq NdArray where -- | Arrays are equal if their elements and shape exactly match. - (NdArray s v) == (NdArray r u) = (r == s) && - case v =@= u of - Just HRefl -> v == u - Nothing -> False - (NdArray s v) /= (NdArray r u) = (r /= s) || - case v =@= u of - Just HRefl -> v /= u - Nothing -> True + nd1 == nd2 = + case (stride nd1, stride nd2) of + (NdArray s _ v, NdArray r _ u) -> + case v =@= u of + Just HRefl -> s == r && v == u + Nothing -> False + nd1 /= nd2 = + case (stride nd1, stride nd2) of + (NdArray s _ v, NdArray r _ u) -> + case v =@= u of + Just HRefl -> s /= r || v /= u + Nothing -> True instance Ord NdArray where {- | Arrays are only comparable when they are the same shape. Then they are ordered by pointwise comparison. -} - (NdArray s v) `compare` (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> compare v u - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "compare") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "compare") - - (NdArray s v) <= (NdArray r u) = if s == r then case v =@= u of - Just HRefl -> v <= u - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "'<='") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "'<='") + nd1 `compare` nd2 = + case (stride nd1, stride nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then case v =@= u of + Just HRefl -> compare v u + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "compare") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "compare") + + nd1 <= nd2 = + case (stride nd1, stride nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then case v =@= u of + Just HRefl -> v <= u + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "<=") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "<=") instance Num NdArray where -- | Adds elements pointwise - (+) = pointwiseZip DType.add + (+) = broadcastZipTyped DType.add -- | Subtracts elements pointwise - (-) = pointwiseZip DType.subtract + (-) = broadcastZipTyped DType.subtract -- | Multiplies elements pointwise - (*) = pointwiseZip DType.multiply + (*) = broadcastZipTyped DType.multiply -- | Inverts all elements according to their DType instance - negate (NdArray s v) = NdArray s (V.map DType.invert v) + negate (NdArray sh st v) = NdArray sh st (V.map DType.invert v) -- | Absolute value of each element - abs (NdArray s v) = NdArray s (V.map DType.abs v) + abs (NdArray sh st v) = NdArray sh st (V.map DType.abs v) -- | Signum of each element - signum (NdArray s v) = NdArray s (V.map DType.signum v) + signum (NdArray sh st v) = NdArray sh st (V.map DType.signum v) -- Creates a singleton array. NB: must be converted to a storable Int. fromInteger = singleton . fromInteger @Int + -- * General & Creation -------------------------------------------------------------------------------- -- | Gets the total number of elements in a given array shape. -- >>> size [2,3] -- 6 -size :: [Integer] -> Int -size sh = (fromIntegral $ product sh) :: Int +size :: Vector Int -> Int +size sh = V.product sh -- | Returns the shape list of an array. -shape :: NdArray -> [Integer] -shape (NdArray s _) = s +shape :: NdArray -> Vector Int +shape (NdArray s _ _) = s -- | Gets the vector of an array. Requires a type specification to output safely. getVector :: forall a . DType a => NdArray -> Vector a -getVector (NdArray _ v) = v <-@ typeRep @(Vector a) +getVector (NdArray _ _ v) = v <-@ typeRep @(Vector a) -- | Gets the TypeRep String representation of the NdArray elements ndType :: NdArray -> String -ndType (NdArray _ v) = show $ vecType v +ndType (NdArray _ _ v) = show $ vecType v -- | Compares the type of the array elements to the given TypeRep. checkNdType :: forall a b . (DType a, DType b) => NdArray -> TypeRep a -> Maybe (a :~~: b) -checkNdType (NdArray _ v) _ = +checkNdType (NdArray _ _ v) _ = let tv = vecType v in case eqTypeRep tv (typeRep @b) of Just HRefl -> eqTypeRep (typeRep @a) (tv :: TypeRep b) @@ -226,18 +273,18 @@ vecType _ = typeRep @a -- | Checks if the undelying vector has any elements. isEmpty :: NdArray -> Bool -isEmpty (NdArray _ v) = V.null v +isEmpty (NdArray _ _ v) = V.null v -- | Convert a list of arrays to a list of vectors, provided they are all of the specified type. -extractVectors :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe [Vector a] -extractVectors [] _ = Just [] -extractVectors ((NdArray _ v) : nds) t = - case v =@= (undefined :: Vector a) of +unpackArrays :: forall a . DType a => [NdArray] -> TypeRep a -> Maybe ([Vector Int],[Vector Int],[Vector a]) +unpackArrays [] _ = Just ([],[],[]) +unpackArrays ((NdArray sh st v) : nds) t = + case v =@ typeRep @(Vector a) of Just HRefl -> - case extractVectors nds t of - Just vs -> Just (v:vs) + case unpackArrays nds t of + Just (shs, sts, vs) -> Just (sh:shs, st:sts, v:vs) _ -> Nothing - Nothing -> Nothing + _ -> Nothing -- Gets the DType additive identity matching the element type of a vector. identityElem :: forall a . DType a => Vector a -> a @@ -247,16 +294,21 @@ identityElem _ = DType.addId :: DType a => a -- >>> printArray $ fromList [2,2] [1,2,3,4::Int] -- 1 2 -- 3 4 -fromList :: DType a => [Integer] -> [a] -> NdArray +fromList :: DType a => [Int] -> [a] -> NdArray fromList sh l = - if length l /= size sh then throw $ CreationSize (fromIntegral $ length l) sh - else NdArray sh (V.fromList l) + if length l /= product sh then throw $ CreationSize (length l) (V.fromList sh) + else NdArray h (defStride h) (V.fromList l) + where h = V.fromList sh -- | Creates a 1xn NdArray from a list. -- >>> printArray $ fromListFlat [1,2,3,4::Int] -- 1 2 3 4 fromListFlat :: DType a => [a] -> NdArray -fromListFlat l = NdArray [toInteger$length l] (V.fromList l) +fromListFlat l = NdArray sh (defStride sh) (V.fromList l) + where sh = V.fromList [length l] + +genStride :: DType a => [Int] -> Vector a -> NdArray +genStride shL v = let sh = V.fromList shL in NdArray sh (defStride sh) v {-| Creates an NdArray from an explicitly given matrix such as the example 2x3. -} -- >>> m :: TreeMatrix Int @@ -267,6 +319,7 @@ fromListFlat l = NdArray [toInteger$length l] (V.fromList l) -- 1 2 -- 3 4 -- 5 6 +{- fromMatrix :: DType a => TreeMatrix a -> NdArray fromMatrix m = NdArray (matrixShape m) (V.fromList l) where l = flattenToList $ matrixToTree m @@ -277,49 +330,42 @@ fromVector :: DType a => [Integer] -> Vector a -> Maybe NdArray fromVector sh v = if V.length v == fromIntegral (product sh) then Just $ NdArray sh v else Nothing +-} -- | Creates a 1x1 matrix -- >>> printArray $ singleton (3::Int) -- 3 singleton :: DType a => a -> NdArray -singleton x = NdArray [1] (V.fromList [x]) +singleton x = NdArray (V.singleton 1) (V.singleton 1) (V.singleton x) -- | Creates a flat array over the specified range. arange :: (Enum a, DType a) => a -> a -> NdArray arange mini maxi = if mini <= maxi - then NdArray [fromIntegral $ fromEnum maxi - fromEnum mini + 1] $ V.fromList [mini..maxi] - else NdArray [] (V.fromList [] :: Vector Int) + then NdArray (V.fromList [fromIntegral $ fromEnum maxi - fromEnum mini + 1]) (V.singleton 1) (V.fromList [mini..maxi]) + else NdArray e e e + where e = V.empty :: Vector Int {- | Creates the smallest possible square matrix from the given list, padding out any required space with the identity element for the DType -} squareArr :: forall a . DType a => [a] -> NdArray -squareArr [] = NdArray [] (V.fromList [] :: Vector Int) +squareArr [] = NdArray e e e + where e = V.empty :: Vector Int squareArr xs = let l = length xs d = ceiling (sqrt $ fromIntegral @Int @Float l) - d' = fromIntegral @Int @Integer d p = V.replicate (d^(2::Int) - l) (DType.addId :: a) - in NdArray [d', d'] (V.fromList xs V.++ p) + sh = V.fromList [d, d] + in NdArray sh (defStride sh) (V.fromList xs V.++ p) {- | Creates an array of the given shape of the identity element for the given type. -} -zeros :: forall a . DType a => TypeRep a -> [Integer] -> NdArray -zeros _ s = NdArray s zerovec +zeros :: forall a . DType a => TypeRep a -> Vector Int -> NdArray +zeros _ s = NdArray s (defStride s) zerovec where ident = DType.addId :: (DType a => a) zerovec = V.replicate (size s) ident :: DType a => Vector a -update :: forall a . DType a => NdArray -> [Integer] -> a -> NdArray -update (NdArray s v) ind val = - NdArray s $ V.force (v V.// [(ind', val')]) - where - ind' = fromIntegral $ collapseInd s ind :: Int - val' = matchVecType v val - -matchVecType :: forall a b . (DType a, DType b) => Vector a -> b -> a -matchVecType _ x = DType.rationalToDtype (DType.dtypeToRational x) :: a - -- * Pointwise Functions -------------------------------------------------------------------------------- @@ -329,20 +375,22 @@ matchVecType _ x = DType.rationalToDtype (DType.dtypeToRational x) :: a Folds in row-major order. -} foldrA :: forall a b . DType a => (a -> b -> b) -> b -> NdArray -> b -foldrA f z (NdArray _ v) = - case v =@= (undefined :: Vector a) of - Just HRefl -> V.foldr f z v - _ -> throw $ TypeMismatch "Fold starting value type does not match array type." +foldrA f z nd = + case stride nd of + (NdArray _ _ v) -> + case v =@= (undefined :: Vector a) of + Just HRefl -> V.foldr f z v + _ -> throw $ TypeMismatch "Fold starting value type does not match array type." -- | Near identical to a standard map implementation in row-major order. mapA :: forall a . forall b . (DType a, DType b) => (a -> b) -> NdArray -> NdArray -mapA f (NdArray s v) = case v =@= (undefined :: Vector a) of - Just HRefl -> NdArray s (V.map f v) +mapA f (NdArray sh st v) = case v =@= (undefined :: Vector a) of + Just HRefl -> NdArray sh st (V.map f v) _ -> throw $ TypeMismatch "Map function input does not match array type." -- | Maps functions which return the same type. mapTransform :: (forall a . DType a => a -> a) -> NdArray -> NdArray -mapTransform f (NdArray s v) = NdArray s (V.map f v) +mapTransform f (NdArray sh st v) = NdArray sh st (V.map f v) -- | Multiplies all elements by a scalar. scale :: forall a . DType a => a -> NdArray -> NdArray @@ -392,7 +440,7 @@ shiftrightA = mapTransform DType.shiftright -- | Returns the element at the 0th position of the array. origin :: forall a . DType a => NdArray -> a -origin (NdArray _ v) = (v V.! 0) <-@ typeRep @a +origin (NdArray _ _ v) = (v V.! 0) <-@ typeRep @a -- | Returns the largest element. maxElem :: forall a . DType a => NdArray -> a @@ -406,17 +454,57 @@ minElem nd = foldrA min (origin nd) nd -- If they are given as Nothing, the range is infinite in that direction. -- NB: must still specify type for Nothing i.e. clip (Nothing :: Maybe Int) Nothing myNd clip :: forall a . DType a => Maybe a -> Maybe a -> NdArray -> NdArray -clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of +clip mini maxi (NdArray sh st v) = case v =@= (undefined :: Vector a) of Just HRefl -> case (mini, maxi) of - (Just mn, Just mx) -> mapA (\x -> if x <= mn then mn else if x >= mx then mx else x) (NdArray s v) - (Just mn, Nothing) -> mapA (\x -> if x <= mn then mn else x) (NdArray s v) - (Nothing, Just mx) -> mapA (\x -> if x >= mx then mx else x) (NdArray s v) - (Nothing, Nothing) -> NdArray s v + (Just mn, Just mx) -> mapA (\x -> if x <= mn then mn else if x >= mx then mx else x) (NdArray sh st v) + (Just mn, Nothing) -> mapA (\x -> if x <= mn then mn else x) (NdArray sh st v) + (Nothing, Just mx) -> mapA (\x -> if x >= mx then mx else x) (NdArray sh st v) + (Nothing, Nothing) -> NdArray sh st v _ -> throw (TypeMismatch $ "Min and max types do not match array type of " <> show (vecType v) <> ".") -- * Two Arguments +broadcastZipTyped :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray +broadcastZipTyped zipfunc (NdArray s t v) (NdArray r d u) = + case v =@= u of + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "broadcastZipTyped") + Just HRefl -> + case broadcastConfig (NdArray s t v) (NdArray r d u) of + Nothing -> throw (NotBroadcastable (NdArray s t v) (NdArray r d u) " in some function") + Just (newshape, t', d') -> + let newstride = defStride newshape + in NdArray newshape newstride $ V.generate (size newshape) (\i -> + let + multi = expandInd newstride i + -- collapse the multi index over the two arrays + -- apply the operation to the fetched values + v1 = v V.! collapseInd t' multi + v2 = u V.! collapseInd d' multi + in + zipfunc v1 v2 + ) + +broadcastZipUntyped :: forall a b c . (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray +broadcastZipUntyped zipfunc (NdArray s t v) (NdArray r d u) = + case (v =@ typeRep @(Vector a), u =@ typeRep @(Vector b)) of + (Just HRefl, Just HRefl) -> + case broadcastConfig (NdArray s t v) (NdArray r d u) of + Nothing -> throw (NotBroadcastable (NdArray s t v) (NdArray r d u) " in some function") + Just (newshape, t', d') -> + let newstride = defStride newshape + in NdArray newshape newstride $ V.generate (size newshape) (\i -> + let + multi = expandInd newstride i + -- collapse the multi index over the two arrays + -- apply the operation to the fetched values + v1 = v V.! collapseInd t' multi + v2 = u V.! collapseInd d' multi + in + zipfunc v1 v2 :: c + ) + _ -> throw (TypeMismatch "Cannot zip NdArrays with different dtypes to the zip function.") + -- | The generic function for operating on two matching DType arrays with the same shape -- in an element-wise/pointwise way. Errors if mismatching -- >>> x = fromList [2,2] [1,2,3,4 :: Int] @@ -425,28 +513,36 @@ clip mini maxi (NdArray s v) = case v =@= (undefined :: Vector a) of -- 5 4 -- 6 8 pointwiseZip :: (forall t . DType t => t -> t -> t) -> NdArray -> NdArray -> NdArray -pointwiseZip zipfunc (NdArray s v) (NdArray r u) = if s == r then - case v =@= u of - Just HRefl -> NdArray s (V.zipWith zipfunc v u) - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") +pointwiseZip zipfunc nd1 nd2 = + case (stride nd1, stride nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then + case v =@= u of + Just HRefl -> NdArray s t (V.zipWith zipfunc v u) + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "pointwiseZip") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "pointwiseZip") -- | A slightly specialised version of pointwise zip intended for comparative functions. pointwiseBool :: (forall t . DType t => t -> t -> Bool) -> NdArray -> NdArray -> NdArray -pointwiseBool zipfunc (NdArray s v) (NdArray r u) = if s == r then - case v =@= u of - Just HRefl -> NdArray s (V.zipWith zipfunc v u) - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "pointwiseZip") +pointwiseBool zipfunc nd1 nd2 = + case (stride nd1, stride nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then + case v =@= u of + Just HRefl -> NdArray s t (V.zipWith zipfunc v u) + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "pointwiseBool") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "pointwiseBool") -- | Completely generic zip on two NdArrays. If the shapes mismatch, they are truncated as with -- standard zips. Function inputs must match the DTypes. zipArrayWith :: forall a b c . (DType a, DType b, DType c) => (a -> b -> c) -> NdArray -> NdArray -> NdArray -zipArrayWith zipfunc (NdArray s v) (NdArray r u) = +zipArrayWith zipfunc (NdArray s t v) (NdArray r d u) = let + nd1' = stride (NdArray s t v) + nd2' = stride (NdArray r d u) -- Truncate the shapes to match each other - ndC1 = constrainShape r (NdArray s v) - ndC2 = constrainShape s (NdArray r u) + ndC1 = constrainShape (shape nd1') nd1' + ndC2 = constrainShape (shape nd2') nd2' s' = shape ndC1 in -- Type check the function @@ -455,39 +551,45 @@ zipArrayWith zipfunc (NdArray s v) (NdArray r u) = let v' = getVector ndC1 :: Vector a u' = getVector ndC2 :: Vector b - in NdArray s' (V.zipWith zipfunc v' u' :: Vector c) - _ -> throw (TypeMismatch "Cannot zip NdArrays with different dtypes to the zip function.") + in NdArray s' (defStride s') (V.zipWith zipfunc v' u' :: Vector c) + _ -> throw (TypeMismatch "Cannot zip NdArrays with different dtypes to the zip function.") -- | Pointwise integer division. Will return an NdArray of type Int. elemDiv :: NdArray -> NdArray -> NdArray -elemDiv (NdArray s v) (NdArray r u) = if s == r then - case v =@= u of - Just HRefl -> elemDivVec s v r u - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemDiv") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "elemDiv") - -elemDivVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector a -> NdArray -elemDivVec s v r u = zipArrayWith (DType.div :: a -> a -> Int) (NdArray s v) (NdArray r u) +elemDiv nd1 nd2 = + case (nd1, nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then + case v =@= u of + Just HRefl -> elemDivVec s v r u + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "elemDiv") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "elemDiv") + +elemDivVec :: forall a . DType a => Vector Int -> Vector a -> Vector Int -> Vector a -> NdArray +elemDivVec s v r u = broadcastZipUntyped (DType.div :: a -> a -> Int) (NdArray s (defStride s) v) (NdArray r (defStride s) u) -- | Pointwise division elemDivide :: NdArray -> NdArray -> NdArray -elemDivide = pointwiseZip DType.divide +elemDivide = broadcastZipTyped DType.divide -- | Pointwise exponentiation (preserving DType) elemPow :: NdArray -> NdArray -> NdArray -elemPow = pointwiseZip DType.pow +elemPow = broadcastZipTyped DType.pow -- | Pointwise exponentiation which forces precision. -- Takes some NdArray of bases, an array of Double exponents and returns an array of Doubles. elemPower :: NdArray -> NdArray -> NdArray -elemPower (NdArray s v) (NdArray r u) = if s == r then - case u =@ typeRep @(Vector Double) of - Just HRefl -> elemPowerVec s v r u - Nothing -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "elemPower") - else throw (ShapeMismatch (NdArray s v) (NdArray r u) "elemPower") - -elemPowerVec :: forall a . DType a => [Integer] -> Vector a -> [Integer] -> Vector Double -> NdArray -elemPowerVec s v r u = zipArrayWith (DType.power :: a -> Double -> Double) (NdArray s v) (NdArray r u) +elemPower nd1 nd2 = + case (nd1, nd2) of + (NdArray s t v, NdArray r d u) -> + if s == r then + case u =@ typeRep @(Vector Double) of + Just HRefl -> elemPowerVec s v r u + Nothing -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "elemPower") + else throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "elemPower") + +elemPowerVec :: forall a . DType a => Vector Int -> Vector a -> Vector Int -> Vector Double -> NdArray +elemPowerVec s v r u = broadcastZipUntyped (DType.power :: a -> Double -> Double) (NdArray s (defStride s) v) (NdArray r (defStride r) u) -- * Many Arguments @@ -497,31 +599,38 @@ elemPowerVec s v r u = zipArrayWith (DType.power :: a -> Double -> Double) (NdAr sum :: [NdArray] -> NdArray sum [] = singleton (0::Int) sum [nd] = nd -sum (NdArray s v : nds) = foldr (\x acc -> padShape sh x + acc) (zeros (vecType v) sh) (NdArray s v : nds) +sum (NdArray sh1 st1 v1 : nds) = foldr (\x acc -> padShape sh (NdArray sh1 st1 v1) + acc) (zeros (vecType v1) sh) (NdArray sh1 st1 v1 : nds) where sh = maximiseShape (map shape nds) +sumStride :: [NdArray] -> NdArray +sumStride xs = Numskull.sum $ map stride xs + -- Takes the maximum of each element pointwise matching from the end. -maximiseShape :: [[Integer]] -> [Integer] -maximiseShape [] = [] +maximiseShape :: [Vector Int] -> Vector Int +maximiseShape [] = V.empty :: Vector Int maximiseShape [sh] = sh maximiseShape (sh : shs) = let m = maximiseShape shs - diff = length sh - length m + diff = V.length sh - V.length m in if diff > 0 - then zipWith max sh (take diff sh ++ m) - else zipWith max (take (-diff) m ++ sh) m + then V.zipWith max sh (V.take diff sh V.++ m) + else V.zipWith max (V.take (-diff) m V.++ sh) m -- | Finds the mean pointwise over the list of arrays. Smaller arrays are padded out with -- the identity element. mean :: [NdArray] -> NdArray -mean [] = NdArray [] $ V.fromList ([] :: [Int]) -mean nds = s `elemDivide` NdArray sh (V.replicate (size sh) (length nds)) +mean [] = let e = V.fromList ([] :: [Int]) in NdArray e e e +mean nds = s `elemDivide` NdArray sh (defStride sh) (V.replicate (size sh) (length nds)) where s = Numskull.sum nds sh = shape s +meanStride :: [NdArray] -> NdArray +meanStride xs = mean $ map stride xs + + -- * Type & Shape Conversion -------------------------------------------------------------------------------- @@ -533,13 +642,13 @@ elements in the array, i.e. the product of the shape. -- | Converts an NdArray of one type to any other with a DType instance. convertDTypeTo :: forall a . DType a => TypeRep a -> NdArray -> NdArray -convertDTypeTo t (NdArray s v) = convertDTFromTo (vecType v) t (NdArray s v) +convertDTypeTo t (NdArray sh st v) = convertDTFromTo (vecType v) t (NdArray sh st v) -- Helper with additional typing information convertDTFromTo :: forall a b . (DType a, DType b) => TypeRep a -> TypeRep b -> NdArray -> NdArray -convertDTFromTo _ _ (NdArray s v) = case v =@= (undefined :: Vector a) of - Just HRefl -> NdArray s (V.map convert v) +convertDTFromTo _ _ (NdArray sh st v) = case v =@= (undefined :: Vector a) of + Just HRefl -> NdArray sh st (V.map convert v) _ -> error "Impossible type mismatch." where convert :: (DType a, DType b) => a -> b @@ -547,26 +656,24 @@ convertDTFromTo _ _ (NdArray s v) = case v =@= (undefined :: Vector a) of -- | Converts the second NdArray to be the same DType as the first. matchDType :: NdArray -> NdArray -> NdArray -matchDType (NdArray _ v) = convertDTypeTo (vecType v) +matchDType (NdArray _ _ v) = convertDTypeTo (vecType v) {- Helper which checks that the array isn't larger than the shape contraints. If it is valid the Boolean in the pair will be true and the vector is returned. If it is invalid the vector is truncated first. -} -constrainSize :: DType a => Integer -> Vector a -> (Bool, Vector a) +constrainSize :: DType a => Int -> Vector a -> (Bool, Vector a) constrainSize s v = - if si < V.length v then (False, V.take si v) + if s < V.length v then (False, V.take s v) else (True, v) - where - si = fromIntegral s :: Int - + -- Fill out any spaces in a vector smaller than the shape with 0s (or whatever the dtype 'identity' is) -padSize :: DType a => Integer -> Vector a -> Vector a -padSize s v = v V.++ V.replicate ((fromIntegral s ::Int) - len) DType.addId +padSize :: DType a => Int -> Vector a -> Vector a +padSize s v = v V.++ V.replicate (s - len) DType.addId where len = V.length v -- Contrain or pad the vector to match the size -setSize :: DType a => Integer -> Vector a -> Vector a +setSize :: DType a => Int -> Vector a -> Vector a setSize s v = let (unchanged, u) = constrainSize s v in if unchanged then padSize s u else u @@ -578,8 +685,9 @@ The shape will be collapsed to 1xn. -- 1 2 3 4 0 0 -- >>> printArray $ resize 2 x -- 1 2 -resize :: Integer -> NdArray -> NdArray -resize s (NdArray _ v) = NdArray [s] (setSize s v) +resize :: Int -> NdArray -> NdArray +resize s (NdArray _ _ v) = + NdArray (V.singleton s) (V.singleton 1) (setSize s v) -- | Shape-shift one array to another of the same size (Nothing otherwise). -- >>> x = fromList [2,3] [1,2,3,4,5,6 :: Int] @@ -590,14 +698,14 @@ resize s (NdArray _ v) = NdArray [s] (setSize s v) -- >>> printArray $ fromJust $ reshape [3,2] x -- 1 2 3 -- 4 5 6 -reshape :: [Integer] -> NdArray -> Maybe NdArray -reshape r (NdArray s v) = if product s == product r - then Just $ NdArray r v +reshape :: Vector Int -> NdArray -> Maybe NdArray +reshape r (NdArray sh st v) = if V.product sh == V.product r + then Just $ NdArray r st v else Nothing -- Checks that the first shape is smaller or equal to the second. -smallerShape :: [Integer] -> [Integer] -> Bool -smallerShape s r = (length s <= length r) && and (zipWith (<=) s r) +smallerShape :: Vector Int -> Vector Int -> Bool +smallerShape s r = (V.length s <= V.length r) && V.and (V.zipWith (<=) s r) -- | Adds zero-rows to an array. Will error if you map to a smaller shape. -- >>> x = fromList [2,2] [1,2,3,4 :: Int] @@ -605,25 +713,78 @@ smallerShape s r = (length s <= length r) && and (zipWith (<=) s r) -- 1 2 0 0 -- 3 4 0 0 -- 0 0 0 0 -padShape :: [Integer] -> NdArray -> NdArray -padShape r (NdArray s v) = - let - nullVec = V.replicate (size r) (identityElem v) - newIndices = V.imap (\i _ -> fromIntegral $ map1DIndex s r (toInteger i) :: Int) v - in - if smallerShape s r - then NdArray r (V.unsafeUpdate_ nullVec newIndices v) - else error "Cannot map to a smaller shape." + +padShape :: Vector Int -> NdArray -> NdArray +padShape r nd = + case stride nd of + NdArray sh st v -> + let + nullVec = V.replicate (size r) (identityElem v) + newIndices = V.imap (\i _ -> map1DIndex st (defStride r) i) v + in + if smallerShape sh r + then NdArray r (defStride r) (V.unsafeUpdate_ nullVec newIndices v) + else error "Cannot map to a smaller shape." + +{- +setDimensions :: Int -> Vector Int -> Vector Int +setDimensions d ind = let diff = d - V.length ind in + if diff >= 0 then V.replicate diff 0 V.++ ind + else V.drop (-diff) ind +-} + +strictSmallerShape :: Vector Int -> Vector Int -> Bool +strictSmallerShape s r = (V.length s <= V.length r) && V.and (V.zipWith (<) s r) + +addDimensions :: Int -> Vector Int -> Vector Int +addDimensions d ind = V.replicate (d - V.length ind) 1 V.++ ind + +padShape' :: Vector Int -> NdArray -> NdArray +padShape' r (NdArray sh st v) = + NdArray r d $ V.generate (size r) (\i -> + let + multi = expandInd d i + in + if V.and $ V.zipWith (<) multi p + then v V.! (collapseInd st $ V.drop (V.length multi - V.length st) multi) + else identityElem v + ) + where + d = defStride r + p = addDimensions (V.length r) sh + -- | Truncates the array to be no larger than the specified dimensions. -constrainShape :: [Integer] -> NdArray -> NdArray -constrainShape r (NdArray s v) = +constrainShape :: Vector Int -> NdArray -> NdArray +constrainShape r nd = + case stride nd of + NdArray sh _ v -> + let + s' = V.zipWith min r sh + sPad = s' V.++ V.replicate (V.length sh - V.length r) 1 + in NdArray s' (defStride s') $ + V.ifilter (\i _ -> V.and $ V.zipWith (<) (expandInd sh i) sPad) v + + +-- generate the strides & new shape for two maybe broadcastable arrays +broadcastConfig :: NdArray -> NdArray -> Maybe (Vector Int, Vector Int, Vector Int) +broadcastConfig (NdArray s t v) (NdArray r d u) = let - s' = zipWith min r s - sPad = s' ++ replicate (length s - length r) 1 - in NdArray s' $ - V.ifilter (\i _ -> and $ zipWith (<) (expandInd s (toInteger i)) sPad) v + s' = V.replicate (V.length r - V.length s) 1 V.++ s + t' = V.replicate (V.length r - V.length s) 0 V.++ t + r' = V.replicate (V.length s - V.length r) 1 V.++ r + d' = V.replicate (V.length s - V.length r) 0 V.++ d + newshape = V.zipWithM (\x y -> + if x == y || x == 1 || y == 1 + then Just (max x y) else Nothing) s' r' + t'' = V.zipWith3 (\sx rx tx -> + if sx == 1 && rx /= 1 then 0 else tx) s' r' t' + d'' = V.zipWith3 (\rx sx dx -> + if rx == 1 && sx /= 1 then 0 else dx) r' s' d' + in + (\x->(x,t'',d'')) <$> newshape +{-} -- | Takes a pair of NdArrays and attempts to copy slices so that they are size matched. -- Arrays are broadcastable if they either match in corresponding dimensions or one is -- of dimension size 1 e.g. [2,5,1] and [2,1,6]. Missing dimensions are padded with 1s @@ -641,30 +802,31 @@ broadcast (NdArray s v, NdArray r u) = NdArray ns $ padRepeats ns m s' v', NdArray ns $ padRepeats ns m r' u') where m = fst $ mapIndices ns - +-} -- Pads out dimensions for broadcasting if one array is dimensionally smaller than another. -- e.g. [1,2,3] and [3]. broadcastDimensions :: (DType a, DType b) => - [Integer] -> Vector a -> [Integer] -> Vector b -> - ([Integer], Vector a, [Integer], Vector b) + Vector Int -> Vector a -> Vector Int -> Vector b -> + (Vector Int, Vector a, Vector Int, Vector b) broadcastDimensions s v r u | sl == rl = (s,v, r,u) | sl > rl = (s,v, - sdiff ++ r, - V.concat $ replicate (fromIntegral $ product sdiff) u) - | sl < rl = (rdiff ++ s, - V.concat $ replicate (fromIntegral $ product rdiff) v, + sdiff V.++ r, + V.concat $ replicate (V.product sdiff) u) + | sl < rl = (rdiff V.++ s, + V.concat $ replicate (V.product rdiff) v, r,u) where - sl = length s - rl = length r + sl = V.length s + rl = V.length r diff = Prelude.abs (sl - rl) - sdiff = take diff s - rdiff = take diff r + sdiff = V.take diff s + rdiff = V.take diff r -- Pads out a newshape with repetitions of the existing values -- Takes the newshape, its map, the old shape and the vector. +{- padRepeats :: DType a => [Integer] -> M.Map Int [Integer] -> [Integer] -> Vector a -> Vector a padRepeats newshape oneDmap s v = @@ -675,6 +837,7 @@ padRepeats newshape oneDmap s v = multiWrap = zipWith mod multiI s -- wrap the index over dimensions of size 1 flatWrap = multiMap M.! multiWrap -- collapse the index over the vector in v V.! flatWrap) +-} -- | Concatenate a list of tensors into a single tensor. All input tensors must have the -- same shape, except for the dimension size of the axis to concatenate on. @@ -682,56 +845,64 @@ padRepeats newshape oneDmap s v = concatAlong :: Int -> [NdArray] -> Maybe NdArray concatAlong _ [] = Nothing concatAlong _ [nd] = Just nd -concatAlong axis ((NdArray s v):nds) = - case extractVectors (NdArray s v : nds) (vecType v) of +concatAlong axis (NdArray sh st v : nds) = + case unpackArrays (NdArray sh st v : nds) (vecType v) of Nothing -> Nothing - Just vs -> - case concatAlongVec vs (map shape (NdArray s v : nds)) axis of - Nothing -> Nothing - Just (ns, c) -> Just $ NdArray ns c + Just (shs, sts, vs) -> + case concatAlongVec shs sts vs axis of + Nothing -> Nothing + Just (csh, cst, cv) -> Just $ NdArray csh cst cv -- Helper for concatenation of vectors and their associated shapes. -concatAlongVec :: forall a . DType a => [Vector a] -> [[Integer]] -> Int -> Maybe ([Integer], Vector a) -concatAlongVec vs shs axis = +concatAlongVec :: forall a . DType a => [Vector Int] -> [Vector Int] -> [Vector a] -> Int -> Maybe (Vector Int, Vector Int, Vector a) +concatAlongVec shs sts vs axis = if not (checkShapeLengths shs) || not (checkAxis axis shs) then Nothing else let -- Calculates the newshape by adding up all the dimensions along the axis - axDim = axisDimensions axis shs - newshape = replaceNth axis (Prelude.sum axDim) (head shs) + axDim = map (V.! axis) shs + newshape = head shs V.// [(axis, Prelude.sum axDim)] + newstride = defStride newshape -- Each array to be concatenated is given a number to index it with -- Values are indexed by array number, then by position in the array - arrayPlot = concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim - (newMultiInds, _) = mapIndices newshape - subArrayMaps = map (snd . mapIndices) shs + --arrayPlot = V.fromList $ concat $ zipWith (\arr dim -> [(arr, x) | x <- [0..dim-1]]) [0..] axDim + arrayNums = V.concat $ zipWith (V.replicate) [0..] axDim + arrayAxInds = V.concat $ map (V.enumFromN 0) axDim + --(newMultiInds, _) = mapIndices newshape + --subArrayMaps = map (snd . mapIndices) shs in - Just (newshape, - V.generate (length newMultiInds) (\i -> + Just (newshape, newstride, + V.generate (V.product newshape) (\i -> let -- Generating the new vector by converting the new flat index to a multi-index -- then mapping it to a sub-array and index and reading the value. - multiI = newMultiInds M.! i - (arrayNo, arrayAxInd) = arrayPlot !! fromIntegral (multiI !! axis) - array = vs !! arrayNo - arrayMap = subArrayMaps !! arrayNo - arrayMultiI = replaceNth axis arrayAxInd multiI + multiI = expandInd newstride i + --arrayMultiI = multiI + --multiI = newMultiInds M.! i + arrNum = arrayNums V.! (multiI V.! axis) + arrAxInd = arrayAxInds V.! (multiI V.! axis) + arr = vs !! arrNum + arrStr = sts !! arrNum in - vecInd arrayMap array arrayMultiI <-@ typeRep @a + arr V.! (collapseInd arrStr (multiI V.// [(axis, arrAxInd)])) + --in + -- vecInd arrayMap array arrayMultiI <-@ typeRep @a ) ) -- Swaps in a value at the given index -replaceNth :: Int -> a -> [a] -> [a] -replaceNth n x l = take n l ++ [x] ++ drop (n+1) l +--replaceNth :: Int -> a -> [a] -> [a] +--replaceNth n x l = take n l ++ [x] ++ drop (n+1) l -- Checks for the same number of dimensions -checkShapeLengths :: [[Integer]] -> Bool -checkShapeLengths [] = False -checkShapeLengths shapes = all (\sh -> length sh == baseLen) shapes - where baseLen = length $ head shapes +checkShapeLengths :: [Vector Int] -> Bool +checkShapeLengths [] = False -- same #dimensions but also invalid +checkShapeLengths shapes = all (\sh -> V.length sh == baseLen) shapes + where baseLen = V.length $ head shapes -- Checks that each dimension is the same save perhaps the axis one -checkAxis :: Int -> [[Integer]] -> Bool +{- +checkAxis :: Int -> [Vector Int] -> Bool checkAxis _ [] = False checkAxis axis shapes = let @@ -739,19 +910,20 @@ checkAxis axis shapes = base = head dropAxis in 0 <= axis && axis <= length base && foldr intersect base dropAxis == base - --- Gets the size of the dimension of the axis over all the shapes -axisDimensions :: Int -> [[Integer]] -> [Integer] -axisDimensions axis = map (!! axis) +-} +checkAxis :: Int -> [Vector Int] -> Bool +checkAxis _ [] = False +checkAxis axis shapes = + let (preAx, postAx) = (V.take axis (head shapes), V.drop (axis+1) (head shapes)) + in all (\s -> V.take axis s == preAx && V.drop (axis+1) s == postAx) shapes -- | Takes an array, set of sub-indices and axis and repeatedly takes slices -- of the array restricted to that index along the specified axis. -- The slices are then concatenated into the final array. -gather :: NdArray -> [Integer] -> Integer -> NdArray -gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [(i,i)]) nd) is) - where - ax = fromIntegral axis - sliceLead = replicate ax (0,-1) +gather :: NdArray -> [Int] -> Int -> NdArray +gather nd is axis = fromJust $ concatAlong axis $ map (\i -> slice (sliceLead ++ [(i,i)]) nd) is + where sliceLead = replicate axis (0,-1) + -- * Matrix Operations -------------------------------------------------------------------------------- @@ -761,61 +933,82 @@ gather nd is axis = fromJust $ concatAlong ax (map (\i -> slice (sliceLead ++ [( {- | Switches the rows at the two given indices over. NB: designed for 2x2 matrices so will only make swaps in the 'front' matrix of a tensor. -} -swapRows :: Integer -> Integer -> NdArray -> NdArray -swapRows r1 r2 (NdArray s v) - | r1 == r2 = NdArray s v - | length s < 2 = error "Too few rows to make swaps." +swapRows :: Int -> Int -> NdArray -> NdArray +swapRows r1 r2 (NdArray sh st v) + | r1 == r2 = NdArray sh st v + | V.length sh < 2 = error "Too few rows to make swaps." | r1 >= numRows || r2 >= numRows = error "Row index exceeds number of rows." | otherwise = let - lenRows = fromIntegral @Integer @Int $ s !! (colI+1) - rowInd1 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r1,0] - rowInds1 = V.iterateN lenRows succ rowInd1 - rowInd2 = fromIntegral @Integer @Int $ collapseInd s $ replicate colI 0 ++ [r2,0] - rowInds2 = V.iterateN lenRows succ rowInd2 - row1 = V.slice rowInd1 lenRows v - row2 = V.slice rowInd2 lenRows v + rowInds1 = V.iterateN lenRows (+ V.last st) (st V.! colI * r1) + rowInds2 = V.iterateN lenRows (+ V.last st) (st V.! colI * r2) + row1 = V.map (v V.!) rowInds1 + row2 = V.map (v V.!) rowInds2 in - NdArray s $ V.update_ v (rowInds2 V.++ rowInds1) (row1 V.++ row2) + NdArray sh st $ V.force $ V.update_ v (rowInds2 V.++ rowInds1) (row1 V.++ row2) where - colI = length s -2 - numRows = s !! colI + colI = V.length sh - 2 + numRows = sh V.! colI + lenRows = V.last sh {- | Gets the flat array of the leading diagonal of the 'front' matrix of the tensor. -} diagonal :: NdArray -> NdArray -diagonal (NdArray s v) = NdArray [fromIntegral $ V.length v'] v' - where v' = diagonalVec s v +diagonal (NdArray sh st v) = + let + rows = V.last sh; cols = sh V.! (V.length sh - 2) + rStr = V.last st; cStr = st V.! (V.length st - 2) + v' = V.generate (min rows cols) (\i -> v V.! (i * (cStr + rStr))) + sh' = V.singleton $ V.length v' + in NdArray sh' (defStride sh') v' +{- +diagonal (NdArray sh st v) = NdArray sh' st' v' + where + v' = V.force $ diagonalVec sh v + sh' = V.singleton (V.length v') + st' = defStride sh' +-} +{- -- Helper to take the leading diagonal in the vector form. -diagonalVec :: forall a . DType a => [Integer] -> Vector a -> Vector a +diagonalVec :: forall a . DType a => Vector Int -> Vector a -> Vector a diagonalVec s = V.ifilter (\i _ -> i `mod` (rowLen+1) == 0 && i < rowLen*columns) where - rowLen = fromIntegral @Integer @Int $ s!!(length s -1) - columns = fromIntegral @Integer @Int $ s!!(length s -2) + rowLen = s V.! (V.length s - 1) + columns = s V.! (V.length s - 2) +-} +{- +diagonalVec :: forall a . DType a => Vector Int -> Vector Int -> Vector a -> Vector a +diagonalVec sh st v = + let + rows = V.last sh + cols = sh V.! (V.length sh - 2) + rStr = V.last st + cStr = st V.! (V.length st - 2) + in + V.generate (min rows cols) ((V.!) v . (cStr + rStr) * ) +-} -- * Transposition -- | Reverses the order of axes and switches the elements accordingly. -transpose :: NdArray -> NdArray +{-transpose :: NdArray -> NdArray transpose (NdArray sh v) = transposePerm dec (NdArray sh v) where l = length sh dec = [l-1, l-2 .. 0] +-} + +transpose :: NdArray -> NdArray +transpose (NdArray sh st v) = NdArray (V.reverse sh) (V.reverse st) v -- | Transposes the axes of an array according to the given permutation (e.g. [2,0,1]) + transposePerm :: [Int] -> NdArray -> NdArray -transposePerm perm (NdArray sh v) = +transposePerm perm (NdArray sh st v) = let - sh' = permuteList perm sh - perm' = invertPermutation perm - (_, toV) = mapIndices sh - (fromU, _) = mapIndices sh' - sz = V.length v - in NdArray sh' $ V.generate sz (\i -> - let - multU = fromU M.! i - flatV = toV M.! permuteList perm' multU - in v V.! flatV) + sh' = V.fromList $ permuteList perm $ V.toList sh + st' = V.fromList $ permuteList perm $ V.toList st + in NdArray sh' st' v -- Applies a permutation to a list permuteList :: [Int] -> [a] -> [a] @@ -830,35 +1023,43 @@ invertPermutation perm = map (\i -> fromJust $ elemIndex i perm) [0..length perm -- * Multiplication -- | Dot product over matrices of the same shape. -dot :: forall a. DType a => NdArray -> NdArray -> a -dot (NdArray s v) nd2 = foldrA DType.add (identityElem v <-@ typeRep @a) ((NdArray s v)*nd2) +dot :: DType a => NdArray -> NdArray -> a +dot nd1 nd2 = foldrA DType.add DType.addId (nd1*nd2) -- | Standard matrix multiplication following NumPy conventions. -- 1D arrays have the extra dimension pre/appended -- 2D arrays are multiplied as expected -- ND-arrays are broadcast to match each other where possible and treated as stacks of nxm/pxq arrays. matMul :: NdArray -> NdArray -> NdArray -matMul (NdArray s v) (NdArray r u) = +matMul (NdArray s t v) (NdArray r d u) = case v =@= u of Just HRefl -> - case (reverse s, reverse r) of + case (reverse $ V.toList s, reverse $ V.toList r) of -- Standard matrix multiplication - ([m, n], [o, p]) | m == p -> NdArray [n,o] (matMulVec s v r u) + ([m, n], [q, p]) | m == p -> genStride [n,q] (matMulVec s t v r d u) -- 1D arrays have the extra dimension pre/appended then result collapses back to 1D - ([m], [o, p]) | m == p -> NdArray [o] (matMulVec [1,m] v r u) - ([m, n], [p]) | m == p -> NdArray [n] (matMulVec s v [p,1] u) + ([m], [q, p]) | m == p -> genStride [q] (matMulVec (V.fromList [1,m]) (V.fromList [0,1]) v r d u) + ([m, n], [p]) | m == p -> genStride [n] (matMulVec s t v (V.fromList [p,1]) (V.fromList [1,0]) u) -- ND-arrays are broadcast to match each other where possible and treated as -- stacks of nxm/pxq arrays. - (m : n : _, o : p : _) | m == p -> - let - (s', v', _r', u') = broadcastDimensions s v r u - stackA = vectorChunksOf (fromIntegral @Integer @Int $ m * n) v' - stackB = vectorChunksOf (fromIntegral @Integer @Int $ o * p) u' - stackAB = zipWith4 matMulVec (repeat [n,m]) stackA (repeat [p,o]) stackB - in - NdArray (take (length s' -2) s' ++ [n,o]) $ V.concat stackAB - _ -> throw (ShapeMismatch (NdArray s v) (NdArray r u) "matMul") - _ -> throw (DTypeMismatch (NdArray s v) (NdArray r u) "matMul") + (m : n : _, q : p : _) | m == p -> case (stride (NdArray s t v), stride (NdArray r d u)) of + (NdArray ss st sv, NdArray sr sd su) -> + let (s', v', _r', u') = broadcastDimensions ss sv sr su + in + case v' =@= u' of + Just HRefl -> + let + -- here is where you need to care about the strides + stackA = vectorChunksOf (m * n) v' + stackB = vectorChunksOf (q * p) u' + dimA = V.fromList [n,m] + dimB = V.fromList [p,q] + stackAB = zipWith6 matMulVec (repeat dimA) (repeat $ defStride dimA) stackA + (repeat dimB) (repeat $ defStride dimB) stackB + in + genStride (take (V.length s' -2) (V.toList s') ++ [n,q]) $ V.concat stackAB + _ -> throw (ShapeMismatch (NdArray s t v) (NdArray r d u) "matMul") + _ -> throw (DTypeMismatch (NdArray s t v) (NdArray r d u) "matMul") -- Splits a vector into a list of vectors of the given size. vectorChunksOf :: V.Storable a => Int -> Vector a -> [Vector a] @@ -868,24 +1069,29 @@ vectorChunksOf n v = first : vectorChunksOf n rest -- Returning the vector result of the standard nxm matMul matMulVec :: forall a . DType a => - [Integer] -> Vector a -> [Integer] -> Vector a -> Vector a -matMulVec s v r u = + Vector Int -> Vector Int -> Vector a -> Vector Int -> Vector Int -> Vector a -> Vector a +matMulVec s t v r d u = let - oneDkey = fst $ mapIndices [s!!0, r!!1] + oneDkey = fst $ mapIndices [s V.!0, r V.!1] sz = M.size oneDkey - map1 = vecInd (snd $ mapIndices s) v - map2 = vecInd (snd $ mapIndices r) u - ks = [0 .. (s!!1 -1)] + --map1 = vecInd (snd $ mapIndices s) v + map1 is = v V.! collapseInd t (V.fromList is) + map2 is = u V.! collapseInd d (V.fromList is) + --map2 = vecInd (snd $ mapIndices r) u + ks = [0 .. (s V.! 1 -1)] in V.generate sz (matMulElem map1 map2 ks . (M.!) oneDkey) -- Calculates the element at position [i,j] in the resultant nxp matrix of a matMul matMulElem :: forall a . DType a => - ([Integer] -> a) -> ([Integer] -> a) -> [Integer] -> [Integer] -> a -matMulElem map1 map2 ks (i:j:_) = - foldr (\k acc -> DType.add acc $ DType.multiply (map1 [i,k]) (map2 [k,j])) DType.addId ks + ([Int] -> a) -> ([Int] -> a) -> [Int] -> [Int] -> a +matMulElem m1 m2 ks (i:j:_) = + foldr (\k acc -> + DType.add acc $ DType.multiply (m1 [i,k]) (m2 [k,j]) + ) DType.addId ks matMulElem _ _ _ _ = DType.multId :: a + {- | General matrix multiplication. Calculates alpha*AB + beta*C with the option to transpose A and B first. Takes A, B, C, A transpose?, B transpose?, alpha, beta @@ -898,33 +1104,35 @@ NB: if the matrices are integers the scalars will also become integers so you sh -} gemm :: (DType a, DType b) => NdArray -> NdArray -> NdArray -> Bool -> Bool -> a -> b -> Maybe NdArray -gemm (NdArray sA vA) (NdArray sB vB) (NdArray sC vC) transA transB alpha beta = +gemm (NdArray sA dA vA) (NdArray sB dB vB) (NdArray sC dC vC) transA transB alpha beta = let -- Apply transposition to A and B if specified - (sAT, vAT) = applyTransposition (sA, vA) transA - (sBT, vBT) = applyTransposition (sB, vB) transB + (sAT, dAT) = if transA then (V.reverse sA, V.reverse dA) else (sA, dA) + (sBT, dBT) = if transB then (V.reverse sB, V.reverse dB) else (sB, dB) in -- Check all the types match - case gemmTyping vAT vBT vC alpha beta of + case gemmTyping vA vB vC alpha beta of Nothing -> Nothing Just (vA', vB', vC', alpha', beta') -> -- Check A and B have shapes (M,K) and (K, N) - if (length sAT /= 2) || (length sBT /= 2) || (length sC /= 2) || sAT!!1 /= sBT!!0 then Nothing + if (V.length sAT /= 2) || (V.length sBT /= 2) || (V.length sC /= 2) || sAT V.!1 /= sBT V.!0 then Nothing else let - alphaAB = scale alpha' (matMul (NdArray sAT vA') (NdArray sBT vB')) + alphaAB = scale alpha' (matMul (NdArray sAT dAT vA') (NdArray sBT dBT vB')) sAB = shape alphaAB in -- Check if C dimension matches or is broadcastable - if (sC!!0 /= 1 && sC!!0 /= sAB!!0) || (sC!!1 /= 1 && sC!!1 /= sAB!!1) then Nothing + if (sC V.!0 /= 1 && sC V.!0 /= sAB V.!0) || (sC V.!1 /= 1 && sC V.!1 /= sAB V.!1) then Nothing else - let betaC = scale beta' $ if (sC!!0 /= sAB!!0) || (sC!!1 /= sAB!!1) - then snd $ fromJust $ broadcast (alphaAB, NdArray sC vC') - else NdArray sC vC' + let betaC = scale beta' (NdArray sC dC vC') + -- $ if (sC!!0 /= sAB!!0) || (sC!!1 /= sAB!!1) + -- then snd $ fromJust $ broadcast (alphaAB, NdArray sC vC') + -- else NdArray sC vC' in -- Finally, combine the two Just (alphaAB + betaC) +{- -- Transpose the shape-vector pair if the boolean is true, otherwise return the original. applyTransposition :: forall a . DType a => ([Integer], Vector a) -> Bool -> ([Integer], Vector a) applyTransposition (s, v) b = @@ -934,6 +1142,7 @@ applyTransposition (s, v) b = vT = getVector ndT :: Vector a in if b then (sT, vT) else (s, v) +-} -- Checking all mats are same type & converting scalars if neccersary gemmTyping :: forall a b c d e . (DType a, DType b, DType c, DType d, DType e) => @@ -968,25 +1177,24 @@ gemmTyping vA vB vC alpha beta = -- | Converts a nxn matrix to upper triangle form. O(n^3). upperTriangle :: NdArray -> NdArray -upperTriangle (NdArray [] v) = NdArray [] v -upperTriangle (NdArray (c:rs) v) = +upperTriangle (NdArray s t v) | V.null s = NdArray s t v +upperTriangle (NdArray s t v) = let - (_, fromMulti) = mapIndices (c:rs) + c = V.head s traversals = [(i,j,k) | i <- [0..c-1], j <- [i+1..c-1], k <- [0..c-1]] - in - NdArray (c:rs) $ triangulateVec fromMulti v traversals (identityElem v) + in NdArray s (defStride s) $ triangulateVec t v traversals (identityElem v) -- Upper triangle form on the hidden vector. -triangulateVec :: DType a => M.Map [Integer] Int -> Vector a -> [(Integer,Integer,Integer)] -> a -> Vector a +triangulateVec :: DType a => Vector Int -> Vector a -> [(Int,Int,Int)] -> a -> Vector a triangulateVec _ v [] _ = v -triangulateVec m v ((i,j,k) : trv) r = +triangulateVec t v ((i,j,k) : trv) r = let - jk = m M.! [j,k] - ratio = if k == 0 then DType.divide (vecInd m v [j,i]) (vecInd m v [i,i]) else r - scaled = DType.multiply ratio (vecInd m v [i,k]) - newVjk = DType.subtract (vecInd m v [j,k]) scaled + vSet x y e = v V.// [(collapseInd t (V.fromList [x,y]), e)] + ratio = if k == 0 then DType.divide (vGet v t [j,i]) (vGet v t [i,i]) else r + scaled = DType.multiply ratio (vGet v t [i,k]) + newVjk = DType.subtract (vGet v t [j,k]) scaled in - triangulateVec m (v V.// [(jk, newVjk)]) trv ratio + triangulateVec t (vSet j k newVjk) trv ratio {- | Finds the determinant(s) of a tensor. Over matrices of more than two dimensions each 2D matrix's determinant is individually calculated and concatenated together (as in numpy: @@ -994,16 +1202,17 @@ https://numpy.org/doc/stable/reference/generated/numpy.linalg.det.html ). If the matrix is non-square it is assumed to be padded out and will have determinant of 0 -} determinant :: forall a . DType a => NdArray -> [a] -determinant (NdArray s v) = case s of - [] -> [] - [_] -> [identityElem v <-@ typeRep @a] - [_,_] -> [determinant2D (NdArray s v)] :: [a] +determinant (NdArray s t v) = case V.length s of + 0 -> [] + 1 -> [DType.addId :: a] + 2 -> [determinant2D (NdArray s t v)] _ | V.null v -> [] - _ -> + l -> let - (c,r) = (s!!(length s -2), last s) - (twoDim, rest) = V.splitAt (fromIntegral$c*r) v - in (determinant2D (NdArray [c,r] twoDim) : determinant (NdArray s rest)) + colrow = V.drop (l-2) s + crt = V.drop (l-2) t + (twoDim, rest) = V.splitAt (V.product colrow) v + in (determinant2D (NdArray colrow crt twoDim) : determinant (NdArray s t rest)) {- | Calculates the determinant of a 2D matrix using LU decomposition as described in the below paper. O(n^3). @@ -1011,38 +1220,52 @@ https://informatika.stei.itb.ac.id/~rinaldi.munir/Matdis/2016-2017/Makalah2016/M -} determinant2D :: forall a . DType a => NdArray -> a determinant2D nd = - case shape nd of + case V.toList $ shape nd of -- 2x2 matrices are calculated quickly with the standard ad-bc - [2,2] -> determinant2x2 nd :: a + [2,2] -> determinant2x2 nd -- nxn matrices are row-swapped to find an arrangement with no zeros/identity elements -- in the leading diagonal (pivots) then put into upper triangle form + -- determinant is the product of the new pivots [c,r] | c == r && not (zeroRow nd) -> case swapRowsWith0Pivot nd of - Just (NdArray s v) -> - let - upperTri = upperTriangle (NdArray s v) - upperTriV = getVector upperTri :: Vector a - pivots = diagonalVec s upperTriV - in - -- determinant is the product of the pivots in upper triangle form - V.foldr DType.multiply (DType.multId :: a) pivots :: a + Just (NdArray s t v) -> + let pivots = getVector $ diagonal $ upperTriangle (NdArray s t v) :: Vector a + in V.foldr DType.multiply (DType.multId :: a) pivots -- If the matrix is non-square or has a zero-row/column, it is singular. - Nothing -> DType.addId :: a - [_,_] -> DType.addId :: a + Nothing -> DType.addId + [_,_] -> DType.addId _ -> error "Given matrix is not 2D." -- 2x2 quick determinant calculation of ad-bc determinant2x2 :: forall a . DType a => NdArray -> a -determinant2x2 (NdArray _ v) = +determinant2x2 (NdArray _ t v) = let - mulI i1 i2 = DType.multiply (v V.! i1) (v V.! i2) - det = mulI 0 3 `DType.subtract` mulI 1 2 + ad = DType.multiply (vGet v t [0,0]) (vGet v t [1,1]) + bc = DType.multiply (vGet v t [0,1]) (vGet v t [1,0]) + det = ad `DType.subtract` bc in det <-@ (typeRep @a) -- | Checks the whole array for the prescence of a zero-row. zeroRow :: NdArray -> Bool -zeroRow (NdArray s v) = zeroRowVec (fromIntegral $ last s) v - +zeroRow (NdArray s t v) = --zeroRowVec (last s) v + case V.length s of + 0 -> False + 1 -> s V.!0 == V.length (V.ifilter (\i x -> i `mod` (V.last t) == 0 && x == identityElem v) v) + _ -> + let + rowLen = V.last s + numRows = s V.! (V.length s - 2) + rowStride = V.last t + colStride = t V.! (V.length t - 2) + in + isNothing $ traverse (\r -> + let sect = V.slice (r*colStride) (rowLen*rowStride) v + in if rowLen == V.length (V.ifilter (\i x -> i `mod` rowStride == 0 && x == identityElem v) v) + then Nothing + else Just False + ) [0..numRows-1] + -- Checks the array in vector form for a zero-row. +{- zeroRowVec :: forall a . DType a => Int -> Vector a -> Bool zeroRowVec r v = let @@ -1052,28 +1275,36 @@ zeroRowVec r v = not (V.null v) && (V.all (==ident) row || zeroRowVec r rest) +-} {- Repeatedly swaps rows until the matrix is found to be singular or there are no pivots which are zero/identity elem. If singular, returns Nothing. Note: hangs if given a matrix with a zero-row. -} swapRowsWith0Pivot :: NdArray -> Maybe NdArray -swapRowsWith0Pivot (NdArray s v) = +swapRowsWith0Pivot (NdArray sh st v) = let - diag = diagonalVec s v + diag = getVector $ diagonal (NdArray sh st v) ident = identityElem diag in case V.elemIndex ident diag of -- x is the column-index of the 0 pivot - Just c -> case V.findIndex (/= ident) (frontColumn c s v) of + Just c -> case V.findIndex (/= ident) (frontColumn c sh st v) of -- Swap 0-pivot and non-0 rows & try again Just x -> swapRowsWith0Pivot $ - swapRows (fromIntegral x) (fromIntegral c) (NdArray s v) + swapRows x c (NdArray sh st v) -- The matrix is singular Nothing -> Nothing -- There is no 0-pivot - Nothing -> Just (NdArray s v) + Nothing -> Just (NdArray sh st v) +frontColumn :: forall a . DType a => + Int -> Vector Int -> Vector Int -> Vector a -> Vector a +frontColumn c sh st v = + let col = c * V.last st + in V.generate (sh V.! (V.length sh -2)) (\i -> v V.! ((V.length st -2)*i+col)) + +{-} {- Extracts the indexed column from the front matrix of a tensor given its shape and vector. -} frontColumn :: forall a . DType a => Int -> [Integer] -> Vector a -> Vector a frontColumn col s v = V.ifilter @@ -1082,4 +1313,4 @@ frontColumn col s v = V.ifilter where rowLen = fromIntegral @Integer @Int $ s!!(length s -1) columns = fromIntegral @Integer @Int $ s!!(length s -2) - +-} \ No newline at end of file diff --git a/src/Serialisation.hs b/src/Serialisation.hs index 3939160..5cfb196 100644 --- a/src/Serialisation.hs +++ b/src/Serialisation.hs @@ -4,7 +4,7 @@ {-# LANGUAGE TypeApplications #-} module Serialisation where - +{- import DType import NdArray import Typing @@ -145,3 +145,4 @@ loadNpy path = withBinaryFile path ReadMode $ \h -> do -- Calculates the total number of elements in the array -- Reads the array itself into a list reifyDType dtype (loadPayload h sh) +-} \ No newline at end of file diff --git a/src/SliceTest.hs b/src/SliceTest.hs new file mode 100644 index 0000000..e63a0cf --- /dev/null +++ b/src/SliceTest.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE TypeApplications #-} + +module SliceTest where + +import qualified Data.Vector.Storable as V +import Data.Vector.Storable (Vector) + +{- +preciseDiv x y = fromIntegral @Int @Float x / fromIntegral @Int @Float y + +stride :: Vector Int -> Vector Int -> Vector a -> Vector Int +stride sh st v = + let + -- shape + dim' = V.scanr' (*) 1 sh + newshape = V.map (\i -> + ceiling $ preciseDiv + ( 1 + (dim' V.! (i+1)) * (sh V.!i -1) ) + ( st V.! i ) + :: Int) + (V.enumFromN 0 (V.length sh)) + in + newshape + +t = stride (V.fromList [5,5,5]) (V.fromList [50, 10, 2]) (V.fromList [0..124]) +-} + +expandRun :: [Int] -> Int -> [Int] +expandRun [] _ = [] +expandRun (s:sts) x = + if s == 0 then (0 : expandRun sts x) + else x `div` s : expandRun sts (x `mod` s) \ No newline at end of file

    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
    k+7`+>64-6rK3@vzj)WA3(2l9TfpZoY>Z z1<%8c!F{3vOX0;3-f_-ImvK*~{B|u1pT37CX4WDUFB5Ji8@TVy@-ltZXYR5k@1I< zzq4Hq*FEa>vaXza{r2aMrcwF)-L~bU0!>j%j7@?k5ad#8=^FO)YL@0-tyY>)CNKWVJuecy4As@UpkpqZN#wi62;|$w z>2I9oiMn11R+e-+QTeG`Xi9 zQ^-Y3cK1rMv>Cjb2q#9Glpi-4noV>$a=r*U_G8KK1~1^%NZU{Hs>*g%&;r|2 z6~#h{)}O_aCT_lzzx~BzXU?M!$&~y->ngc|k&xnaioN_ajZNR?Cpv>%$ve;2%`lz} z2G4BWzoW~ZDf*K)Q4A!e(uoBMf(=&S$>m(oLa2UFm%6hie=39;c%g z_F24$X>m{~6Drf3eHOCksp6aP?a|Nl;U|*jC-(>P=Ur-na#iPd!K&UsL+`#tvcGJ? zNV}Z@h<>yN!3r+?L43mXdUXltqbH5DNuOrRpO|kX58@{5Xa35j+CeWa#$2L8JX+@! zS~y!gM;?r0@J6JmCmNNA7n>+h()@%^5y2^2{8`PSFWe2YWpI9m%EYGN)Dimv&xR>}; z8U2(Tw+z0(-QC@N1Qk`RSRzf?9l@!Y8tA7;Wat{h$}V7ErWmfY(|qS~BLaesh8lId z#LROpZRj8)=y#5@OH3BXq0lYo-Ru2@F9%gXO5l+{U6GuNyHBGA625N&vIb%s8(a`ZVvbX7hU?R_-{hXUa>`oq zt{r5FY8vhXNj&N&3(l&5f_i7mSI&1l65&~DcMU5m>@t6Z%5&y-pYJQ}9;rPUZp{k1 zI@yq!cU2Q9u{qA{U94{u^*oP75HBJ`eMJKGPNSUf>mRBdkgR7lDB>SW^tInSs(1vb zz46>d&;*CrU-E4!r9^K1P@{3515`Z;!nMXH64#8v4^-}8nhn2SDd5KOlS}bX4xQ7u zjtbZSDMV81TtF{+{7o+6nt5IkqYmtamM=?4chIh#l^98u+vl`%(pU-RaWc$qX(gs^85SOBX9dr%QL3Ho56(9 z;!=KUHbLb%J@2@N0&?R^t^Dw_S=of@8Z&%u84;S88HPOl@zPtl$x0`3Wd|`;BGl^A z;+_F|+0`tdh}WE&O%htPf$48mU&U#c>VzvY| z`n27{?1>}^`?0w`dA)u`w+z9EpQ}`h`He5`Qgm-bVpz8~V{aOR>*93tMhM>JBI>lR zJ_XZ%-1h_<>=FI(xM*vtp$hDsfEIjOimiD{6oVzqa3Ww~O1SIiRJBH{wz-2|Q${+P zx4?S9wM`)E>IdrFAp?J4$gJ63+F7tR;J`F3+4j&&_h}1|yGviWXm6RcX;0FJbNdQT zDv(%}tqWRNT%A+9@2e#lw>2fAk%kROrz#GF9BEu84d-W>&x*CVsZ2)L{T$sDIL7m1 z%c*}N0r_z7_`g>1OSNN@W(LS*qot~-C#ps;bzK2BQb$)w!)nPWW|Jp*e!0q?-)) z0$NU5$Nw&K-J6c%%hxxNx9GxRy?e>^e=uC7(zT~B)isb%0b+aSSEqR8yY2)d@^j?E zP6QI_*ilh|sTqif(7~cBaMf-Tq}Kc>*crv_R!P99x+Gyxn?Z^!PUu z9W9!G+dZ5WAMq?<0UIGDO9KXL4>L*wH5u%j$I-_6T!7^GtCo*~e98`P$fF)US)w;o zODA^Txq;Izu!kSK7Sob?uhlR5VxXv@r4y?Jrby=2#2E+Hn_&*K7VfP_a?rC~BlS=B zFSiZhsM7u5-1%)UEq+`VPkvsp2lAWmFrvI_w3^mgg-Ilg+s*3r{@@*rhd}qwaLYhW zj1W$)<@dPGo_qqW@fdR=>@OYhO+!_ve(#;(NlrSj=D}>UMj2t0Hcg3oxGx`_jP*1q z!@#d$tl%C)wnan3hMFSWbU1-CVT+kor90V4uoCt+nL*sOn3QN_{-?W94oC!uW zw$w^vuH%z&y9Ym|syZ|??(rs=8sQ(o>QCShDoW77W`y=BwR;>7jUbK)gYb6$gL+WC z1Fplj%9r6Ek>Gv@hWeP^Jk@!b!Tq-89rwZV-DT3*TnO)W;=H}?<>jvB=KKOXhgBJm zh*7Bgf+Y-I6c~U)!?GR87AlaUNlcvz)bASyzWy1f`5=$Y4_4>ez5M~ymP}E+3%l;zREvcKdmXuwbHY4(vTyuTcCTh8r~~ z*`x#5-&JBe?!8Fd&5surcPm)g={63*8d(Qok5g6Qu$}caE6vgMlEH&nN6eX=YmI6^ zplCeAePyZ?rkAZ5ij&)vIg(gvtXw^?4(0e6aWYJ}>;y?O{dg7#;?NyiGHc)*cVucw z(ZFAyeRF6TKHq5)R!tfy6ziOObcgzm7JsnX|DKGzni>gr8){v3r60eX`t|YfXVk*6 zQ=Mxr+Mq2tL(f}C&(%->H6spuGg>QDr~DF zReZ)kST~{@r28A``-uSwbuZ)CKEzbt{rb~H8{yjrYjiD{@BAc>+&{E=y$O!abBf}b zaZ!+pU}*z>du7Lwte0dUbKmOS;p;{NPR2$-n0_MFD>v<`cedzS5Q1Rk#$Tn?sy2D0 zrBN^ur?T+NXZ72A2&w6`9ar9!yZAi%fYmIeDO7o|U%qC*#rZOX79{{N zV&Uk2`aNqs9^+7XVerrPUErE}auq1Js;O%-fIQCtQU_MCp8{CNHDxILr5gRX- zDmD};u-_eM%Dc!=Fu*Ohmv|H-ofgp zrU|SD;Oo6X0V(X?X_>b(-YJRV$S;sL3quJ5gRwCK<)mND%s&rwZbf-45l+~oYb2^b z;x5A27y2(s-a2)b;8hDwJ14kFKU_<*TH>0BX!oG-q$WcdVv6e?x|D3%is^}G#PfB& zruj9sL^4Bfcyu-8w4XRBmuOcICGxJckd+b#53Yo56ZCsm$*xjp{`!Qe<>(;Q6Tgh) zq7|vl=G6anZS>_E*e4hXKF?$`D7f>~KInDSCVFTHh1~yXs^ZoX-UXrJ1w@i`t#83& zKU*(tNoRX2x83c=%VdHdw%D_JHN`fvh0F+V!`9%UTZTwbeeyJOM}skc_^aKK#~j2T zyzzO)O};oH_wQaTf}7HoqRlJ1NDO1tRw*CWh`S*)-~J+OrhQ^9%xFc}CGhA|M14Az z85AVmP{aF`dc^C+;C;>4q!iFXgQ|nVD;1(a>axCWx7v(Q37OLB9WP=z2M!@rt%8@= ze9KV&I~TR*Tbsg~K4+3&9I2xVA*dGXGO&K{#+ykc9^>BHv5naibzO^Ks$n>=enAVF zGg6!`c*atBx&?X?%bj8h;mL9oNzP&V5b9CV|_XX7h=05^^-coUB7< zaKM;3ROwZ@P5D_PDI6P1f(?(n=Q?mtS7TwvkatH&sSUI*@F+ugJ&gK!LEmHddhfF` z!Jt--RMX^TNucdH)7lffB z`RwO?zk_2+og8iXp*yauC%;poq<7{{S39)-MF!0YYxfT-wndY%l+zrv?cIUf6O39ufMCg+o1QjKn=7oq2ksm=LQOB%m3_zFK+xa&^(d0A|JocgKo z_?aPxVO!D;QD-Bed|l2dY5Q*zU&er0+U9L}K(KN055|jmljW^@>k@=m!Mhq9koS}F zr7kI-wdca`2WQ%Qx2kCE)bY3N8ec+PIH5#+rb|+zPai+c^y&0kcOM7nI)=@kdRu~` zp|;Oz7QiZ={3k{vJf*yxq;F_@QfZYhOVOsE1+b6VeqZ+`<9(?EODwi_dv8U&_0+`| zXx?7v$%>`8 z&7$7$m(mzkuYp_krWt;Jkh`iso|*__gO|c95fkQTF_elPFGHST4lop0|7{F?`jq3p zKAF=pX{O^4**;UN^QhWOp*n)$#mR8iTp3x~^hg#LV#0}I1;$dLc|iTKiF1;jN0ag? zUOuS=Wz$(+phWP9;0*FM@=S^V(n76v9YBP$)t@eYIw(GA`5nS&WARuGQ{1Koq4ev@ z6Yl|1y1JnuT1m)3VmzHaRfov8{JGhBofSP14G>DJs5vcJ}A{JMGe6FvikX$HwO zj$hxsGnVZlYiW#zGD$|18(&%E_e*ePCS=)g_s6x)SaC~a zgpX6F^eEJm1NVh`3oT)2yO)xtz%kx1%y1sqtMHuVYEUZ?5^qGP z2jW$Ud-=K-dP#HDU&Z2!a!M;lxr^XF`%Bj@yw_M-*-WO)C zEIQnHcMGPq!RzvjWH&kRyI(d_dwcBgWcQU^1)H=XP>1!grm7Kr7fVkn6%st07MF?SCvDq+PCOz4iQBQY_2q!^Hs4Y* zIc}Byv5f1Ae+8~WC`ARFIy~`2C34+XBN_B6si{8u)f%JwDREXC2vt#J`0i-Yo-AHD zgfdYn*+8~oijlZ)^Os-NBOR1L_TD1+ZS9Hp_`cYuu%KICe+yr}823vanCMUP9l7Ew zu+mgvqyK=dXNU$7X>0P!m-yQJ%3bu>H$(RK6$Nt+WWm-`6_cyWBT8OUssmkL?jK_5 z==Gp+#~E=dwqyD2CdDldc!$a^zs9U%!qn}%B&BNGC+iorx(kQ3tDD*)#(qV&)q=m# zYN$=?4*sdDPhO2(KA-*vPrnDmOtAnQmNOw1p+i)#9 zk_pu651;)Fmb*?fyr#7bk3-F66Z9-NJ^JP===(z~#>t28l{zWK-R%5!x*)kMT`P-h zPwEHq>gi0aXP`vNuf(PWH(xx(KjG)RwE%X5*QRHRQB5>FtOvHyd;w+d>r zecOIR@nS6nibHXycyTC2Q;ItjiU$bpPAL?EdvUkokPuvoyIXK~_ae{A^IvOYt&Ml> zeD8d-X3e{inan-8Gn3qxoY#3C=kJi0{p8LTydYKjrdx!4f24$|rdZ&6d$PUkAZc)b zhr3g@P3UU3pgPcleg#xkEKA8N?+P#J>`q1Kx~hH=%*b5Wh&Xh-Ai3=lF}f+E&i5kj zp6VT<%U>S+hMWI#<;y9zIw{YNRE4OJdSy46HyQ@(XdL3i|k6$PG2w;dU*v(UKb{}O`f!`~ggpK0+=d`cl4rAnJai_HuWh9r z+~?RyP1Y+;3gM3O4^Fl40R4(0PZy;rMtFSh_@|qjV=&oD-|IR|6K5~RK*s1VK9|~4 znp0bZ_a3tqTita7^%kP^CQ6OFA+&_pYiqL(5n{5`KiAhw|HLh0n?qP6y$JB8hv_2_jNZ!tiSekm^lL>1zezJtpcb%^C(~zP>wXTH!}QG z3x=f@e0&)k*C}y$O|FREb zI2%ny4B30<$8Cf+aGy^8f$mh}t`d)jPSamNKW1cl{3 zIub~-zuy(NVfS-TYMkDg0>ebfV5D;9u;_Ji&3^!1DE*JJYVdQ3bdn@nkMaX@?X0P? z_ZUy)yr?EgE`eP|Up_DsHKCX;B4Ze+i1~Z{;~3?*LKwAKHfO4y;?`vs-0=FZhCmhW zzW&Ulp$*A}*Z|oNs0X2J8?Uq4pZ6<`&m{f7;nad>%WW|H6bV>ypBra8BEjzuy7?w4 z!!>=}0g3>O>M0|>hseYIn4!k&Jg*sn|9a7^a3l(lCb8TLVzCT1;Fr&eO_h!Z?O+9T-hE^WZ!Dh#=aLb!_2rUg;u>% zVNjKTfvpPPn84w$2YkzKwlauKG<|adXi$Yoa|4X$`l*q&J=1XT`ZCvy9SM$6UN49l z{~4ebU-F)@|AiDy2CmD^mahh{DJ?FscX+Y0n&M{6De7fqHe%WQ$J!xiO00xQb52T^cn|jijQs9 z+0j=4u#?T%p8ztHp^}+H4VXQW=Amr5Aq?3!=~*q;C7h4k1&hYID+bd1d5Sj4wi{k+b@DDf&jT(1=XJUMd>}<(}V}vQZU(*a3Gb154%2L7g!f(cbHA zIOp2=RmxF7l}Xnt%Z6zF2u;v#TyDycGyVJGfwq9%75#c_GMp@B%O%gZKytsCqKsR2 zRU|J!VfMf(+>`?+@6%YVcn9%dP~@C+sf|0iIgqQ+$JUC1>h~>Sxk<*GKt-w`$q=pD zbLwzE7pm6enxdsKIG#lW_>p=}lz~&8xk;f>`PVvBRp7+&G zlTpj<|K=~uv{hd2RRIF$>HcsSC}TzU=PlPX0?^EIcB@*I-PWJDjw1{a*f@HvLAb}k zsAkS);%CB#r>Zg=K&*1~(Hg(Pc~p*?Qlb9-PRi2zXbf-j0PGL^3oSsKw9$0X8)?qW zg$t34^DPSIYaq4DqmwLpeFP&1l>)|-XcXe z7Z*r);PviZYQ(x9i_LMV7~hDi$C`>8M@`Xr=mG`Lr~ZCXskJzj@fP8tJ=yj;!UKM` zSMrQolYQjTkgy{y%-SG*H6w!&CxI4??#oX2NR%N-jAQ?jNMv+siH2r%JZTZ|P#dE6 z`E^68QcGa0D&@rjnpvfE0d(F}BFXe`(?N_~=#z=yAG5hZ4$`VATJqrlOl!G56=?Lx z@!C`ujzgUx5LJ%il*Qbnn|dvS`f8Q%&^s!$s&KzaA`|4}=AEFJUe;UxN%v+q3Gz{K zbx%b5kxAwS|3Nsbs*xI;EyEdw*4$^!k7bYQ3sCFK_pRhvG-%enzIrcBtG}f)ZWuC< z85T^60Gh`yWAKd11HTn7>KY87*ET>DskI5{g*mqBoV#OrRj4rSw7Gvg>VY@FGJ2uC zm?Q|_QbI$LjkQ;b7Nif2XsD}IpS8+H?4_%E3PAOiCu?jYZuE-Vec(5QD9mWdAM98t z*S1P_qOMN={>*kbyk_59!I^+sKbE1CayI4~mK|;>6SqSC8#>cjq;*J!oc8TCrjAb0V{t2G6wJF@HoDyxVW$^{N{$qxy{{ zWUvw=$fh&P?uOm~+$v*k zr5cipnQDc8!wY0{DfQ%$+%HbBZ~M`F7LM_X@Wn(;gR|G@#GE+0J_NA6b4C!l6ZFj% zo8&d-fCYABKNDiWc^P9bV{fu=~`vMfvvKTUuIj z&t(}!6Hs1NDh+j9%5MUx6gkSvIXLXC8l~}borDwnD;r)lg{_ZXj8tdD{cCC`<&FHx zIcGjJrgbYGIV&v|280m0SX@@an$yS5mtQV9)XFCT)KWQoDC2Kd$pjdQP$*Spa1_RS zD8cj&&c9LI_Y)30DW?8Bgw`(q6p-8GAbNieCuhs4Vyn`&- znWGc_2T~!Lot>=;(+8r+c-e;9lV*Y!I~=cBa1 zXjSMnAIO$6*(C8?EG^>2o)b#kyri+C(;T9e5W;=R8lcbr4?wCuqkVS7zxdZ(Oc$d2 z;;#4$eA0iqX?d8hv(&35r2b`G)hJo;_Dd3@-wR{*Hej7UDRZiKQOZA@VLQzhUer4( zD>5_-aGrtV+AjoRj>2SD5Egxdp)2@3Dl%o=oMlyM?L92Ay^;?qRQ7o_cYHd@!Wi;Z zA$eSGSg0w`!|A*9Mm;zEG;GAGYwp5Yr=CYo#c8+4jRXA#iu+qWh5kE^pvdkt%7}ZS zI=p%4$u4D&%Hhck$*dUI92b-F&5Nen*wxM%P?pIA?9nu@jl&h_mb zbxDBiKhupc_WgNs2OV2LKhHX%( zY3RYmuh`Q^lqOj|(+&~lLj$iFJCIPFg73~eofu6(Q3D@#-HP%x$O?iHn^KMTlPnbg zHIU7Qe6j;7)~5-JA6nPExkU0%G%A+xSlg`i4Z_KH1ie&}=&Odi&$Gmls=8-dzcKuO z0P~h}_i3xs&{j!JT^)@YSc|SXVK>)1Jv4g}-Yv@42f~Fj{6l#9b4Q4Zq>AJEwH2*l zAe*`C2NOA2(SvNu>}7`w9P2q`S%o5_z7kP-7gewXckiQwD=}3@^6;n)sfwfTm?;B^*>*P;-`MX-e5D%9bdDL0+S^jt< zt^u66c5Q5)=Hy}yi!hX-v?Cb&O1DZofYT6}&^a*Xf*iW~QiKCAX2S0B^8Nz6F5Zka z-*z9=tPf>Zs!(mC)BAu}hq5D!k98^qt8%WV)%_#93({S66)57L{o+q;Zb&%z~>Q3Bw`U|dJOn^NyNEq=8z+&C3- zMIwmSLRCi#P~o*&{@(xrRIB}2GeIYrg|1P@k}ja3~l z#j5Y!A>9|+Xtb`(rAtfpHb*Q4e3+fj9``EQu{ceOI^A9rqt8<`9b}>G+cYhrpZn)8P51~6`Ms| z81UA3-*BaBeZ;|Fs!FViAd^eMS6m`Fmj5gpI`|+M!qSa3_pQ^fcy~{@N|30BMX3yzCyoQ>-1Z)bTy zQ;X#_s4sF72pc|1TqN0D@9UE@7Kd^ADi<0bGIRL0+QO_wL0uRdpwSQxr#fN|3XVbFW>lqO64@Sd<@_4VE^|aQoy=CA~$)w z{_TyTlg&2>)`d1BJCEqE_~9O@F|t39qZ8LwiG#n0n}gn2+abgIHv=!BvAdPM^z7L- zjf&P4hj&O7jH-8g^BM3lMr^JM7Uk`STNQE*g3?p!To$M=vJW8rvPA}XUzIraRVtts zxm00F5}kbEL7Hw`H~a&rztGXsJicm`SX6Cnhu6R$f45M?yB&WIuuuuTC$saJ;iP&B zApRz<)hdaRI;02i)!THOwY>iq>S~5Y9Y~=y{Z*2+@&_BE`SZT7c{Caq z6&|0>ZJT7MEfRfrz^qim$YB|6YesTI>hlxB~_To^2>CF1Js9tT=KJoIbw86vKU zC!D~7{O;~CXTvc!Iq54sgtQYRX!0HCq&2aV0OQn6p*{@)+7 zb;k3EWuVQoIRQW0F%B}ngiXV`=FFlmaPaRofm!iHj$Chs3pXl6ZndYBLo$+`WE|ZM zTZdox%168B-U9<#W5o%SdfU%zB}0!Mg)E#S@_)xUF(PnM=dEbvDDA#Ju7z~G%!gaZ zlHg?6!`6CdDxgnH2D#KBmSHlKL!KwCJbYVyy%HApjcb*x*~B^nXHmZkTT*-k*a7dB zaDWlNo(&rT2?u=#wy+X=a87fS`T3t}$;F$kf$D}B^i&jhNsozu%&)=7IKj1$SG z2(gNmxunHY=Bn(FEBnW()AsG>|BLc3cGx39J!92+&e;GN z@BC#NJMIFMX1CM7w#{;o;2b`2&Ehl2E%P$ucF<xUYNb|+I)omFNQBgQD- z>tb=Er!0J)7^IiV=d%#B^54kG`c?HQFMako;4EMmh+Hp_HX`)B4lP0@AOv?Xbh(W9 zCf~dXBmb47N>bF(S~UKQqCCyzWUFNN1otEToGMo=qFk9K*)CT8vUHO)F6oOn_OT9P zaHcB$x*|s?ER->$SFGowv!;+LHWgbB;cbEnvPxBT*uY61Fn0}Katy$@YE@A>%ZX6? zzB9Qz3>Lz%;aO!=0tV+m!}@H|ROR$C%B_h=O!cBMofR2wnbJK{gpf+Z5C@Nx zzYA5Ux&Y@ zb!ye*fdK}q*8xm@ehyDyhl~3HuamNM%LmJb>;)Ym5yLdM5E#@wp^vgKH(us~#014K zmm&;vHLXAU)4BQO-rtG?WXaox>-e$E%+Ik?XogvhOEoTjP|NV!2B3 zh%J~o)FQBtuh)oS2}v5UBpr=-+(mhF@o(@e!~UP(S7z?~QNy@u zEi^NG`Hp{7zDrQ&_A9HO&7o3)B1~CClyogkAx%-iZ!s(?^<#8!#)T(waFy;?BcW*M zkf>$jv}H@}^t%uvt->Ez@`HvUYhAQkF|)0o{u3b1kbQsS$c3wxI-`Uw7}cqW7J1z( zwTZ~Qer{g9WMt=Ver%^tm^yY-w(Ys`o-Hf_7V3j;Cc7NaE_yGOE)thUw(HnRumBX{ z!s0xJ^)-bD{?uniZN-%A)bL`PTMdIxMk*O8LWC@zzsnETmZ2QvwOW=f>*+5KIWW#Z z-zeHYn{zUJi&TlUSNEnQZ%%81lO{(R@-c?)h-M=oX5Ks@88)=_5o?C10-*QW*lF^!Lk)#JjCL=71 zF8Jet6b>;*lWc<+&JrG|RGyh#=QB1l+M~^>^TxU z9oN~HKcRu#^}c3r5!eP81q@$u<65U3@p?+kUMl)ny8`u&ts1;GEaD=+X~nV=Y961U zkWsq*@w5WY@%Nl*ELrP(+E3O1!4>K{l#(x;Se|xX`X9F=kcqlh5lGqL$iGJ|LCm-a z+b2}rvz%;&teYcan@h$PLYa2E=;3mlrmAHV{~NFUEOubm3grwmQ={uvh}S4 zio1wb?E8R8_Msx(k6Wc48a=tvJIk({JseHCuJy_8WQhnD+46KaVwg7%NZPDTG&or!nln5tAwN8d-v_ zc>c5qMAijVQimTuPMOZw#UoU30_;Dlb-#J@B0`52fSi5BW{GI$R~01VyDewt`b@c{ zMvPthjIBM}btv?&aztNrTz76oKf{`3m7%r`r@#H8={vuYw2%8LSBwef`$=TeNmDfq z)(_UAM{+q4+345{26gnA%~rQqtm6!g*ml1$*kcdyC@qld(!9)Ez!BkUT$zCxYjoN?Oe%wQu*6{fEa=v$fSAYn>jcEBlm;dvLKq^_G z|6Kl`8~^9e`TzaMQ5{r4qsh%d|8JU=FDmB#c3zd(*#3)$G>i%N^;xgNJ)r9{oNgO& zH+fKq>5m(TVX&vr+>P}-3{+9#A`yYGr_B2Lt@}{`tEUC^k#ognrUyKm$rJZ09f6X& zcExdiWxCsL{U44v=af!^aSB5WAwrHXwopfTF?V&_=tq=pSEZ2;xS@??*o-ChnnEQV~3YdAwmL;1Q<-A5P>IP;= zi*%VpYX0FX!<8Jf?GOw6x21Bln5a+jApWs136dGNoe9?`%N4K!A4?cJ;Hce`Jq{Lj zg*J|}GdCt}Dx9LFW8PF}GWI09uU17Th@@aYJh1bf+xY=a$fd~_o{!2D!%1zvA zj~U}IyAKUW?G7$=|mY1e&{xcoh(5m8&mme z-Iju1FJi+^c-f=RnDx`P+|cGVSE_S=98&*WZXB|QFMPeflJ7vW&sy}^}(U&Z_<^6Wn5CKD zT1^jUL!7gUdfpKJE5R=_CjNm$XJ_zQcqkeh`i|h=ES1>y8w($fz6lYo}JRFh-n}z zC`2e%nHy+=wS*-ZCI%Upc~r}>o?~|?w@ot&>0)sro>_9@F*fAEq@?yMnHLgdDQ7(Y z`SYv6CuIkfZ=TrjI4cdx`48`mPZDB1F(qc2rI1A8yVIWcckz7D3%)*7L_^ob`CiaR89^kC%%AsWHD(EyLNU<~gnBT$vch!{$QBH1ZgN%>L0 z_YkI}e(JECIvNw~-2Dbl#ga;cPHNuzb0cUBUglGIDMSCK*@Oafh7c4&F`L3@6!wg0 z={?)hoEH$ecbW;~xM?F30>|t|AzPE&$0vU%MUegj{8kewzq3uz0iX$4XYfW@$%fgGyMmG9?cTeYuB zzbKHjDQ1DIyu+%yP)p;^)3NEW`T6{7jo?qbka-2fK;Vm_~lzGJ_jQERDO zda&Zx^$b>SPc^MeQv{M24WXr@7;cd)La7PRc7Bp^j%Ny&E!){l=xOUeXL?B{piO!; z1An&=GF?+Cov1aRjVM!BnK6*Ok^f+Q8fM^3+~%wa|7`9^U}#1;G5CP}2k2^yG#pcW z$&+hfbOB^{!aG zSx2qjzldw}^WG_*epIutD7ueVAUr;nn5?n0!cOX6(flUoInZm<$T&YY|7G7G;P6`H zC42g`uM)yPs>rCdI?JG>yOvIHJnW56!`hj+c%MK85*MIvt~%iJA&SX~(h39~mZrr; zoko=j{`4RWD%w6^=Q8q93GH;SXNt#hkNt!@-t?+3@MSF4qb$Ac4UP>L=`tGkc8Fkt z183yZNOO}%FUz)H@WN0{nXHve1?W;9#Gr!`e;`97Z;RW2mL%p|ye&rQ+M`$B@_onW zmq~(e@+hHNaEK|YnK%KO>vf^!kB!;A8d+i$o#~^i2yAXQ)*mD_o4%3QI0=y%u9Vk% ztb~SnIW;Y?X-@T>KRXoZAPjX~Ly`B$1yRjbsC}VH_V_ks=PB)Ny5(_m{dcFVWtQrV zj5J`WGh`P!Mp$@hy=FiThCw-l#%(p)f|p8h4k6xHO!ygD#L*SBgy@O*Cz> zZ4oh}g!)86+O60TJrArm&+P@SiQ0^zEpYFrmGmt;1vpmmYBqn-@+RpfddGp!^yFQz zGUTrTAAgh!4x2(;C;bt_3mjr~0MaN%lFcxtI#Zb07CGCr`oj1ZefI5EK;%1|ZRSp$ z$mM|y?N}oS**8*mh;V(oYMy4L=n-~HYCMGN&FTvt=-=dbZtFbSo}bPuX{)fH)VUB_ zSMQUwW>mO%R$a+Blin0?{&#I{M0C843y#Sxv;->Ki$GWkX#d|M%Fvt5qx#yhZAGrm z%(y_^Ctg_!6ai)1=jtc+n6uMxZULi@AIxh!H+aa%{x~)=TZuLiQ)gsL8-@JKOaJf0 zSt7lbC6YF%MN@QsZh8v_6?Bz-rNL|ROH-VsNh+d$mmZBxhDk4@&LdSgDZ* zLE#Uc5d&`}^zZY;b}f&r?7e!O(v*!3ckhbY)ru`{{XtpW`FAuu)C4M62m1c$4wXho zE2mi@ZDSi<-}<tXTSZd1)%uxX&Q1)A%U7K7N19+6>QW!$60iR@eP<%E zotfkxUeEo{b!T6YSwH-ifPA1`>{pwnkfj@&$x=f4Bo=4vUZ+(wk1C_QaG#~Q!+_q^ zt=G3;`(z~h$|}aQSnk}u{p?Z!vM2?XG@h(9)y?ziH}>|D6N-Irhw@YGO&04aL5si! zH+sMydB?w>($ags)lAVDlqBuc%Q;iH2)?EGD@Z_mc%@-~i*(4TI-G$Z7?EPk*4LZ< zfbmBe+p|-xv$3!i%43T13%0ggEQ17wkd|t-aGBuS7)niY$zU;qfb~^k>y)T~;{CVD z>=@Y&rK^Qoallpl)NW6JNxUs$uhi!Dt2V2s=>5|Dt;$`%1~zoDyXEY;BrQ^3yeIMP zTkpTia-$Q+*hJ=!5EZdlOk%4)ZD{9cP0`80U9q0(v#W|}z0aQZ-z?ibB1~&^V;F=+ zl6O~HcOVW1t5uIQm!|sQC-Ad;*qu&i7HhidnOkyAp#jMKD`}|b^D^FKzXOs?R3pzO zic}$m`VwrEWd9*2$t~1J?6*-&Q=c7_x|Z9gaA;}sT(XKbjurZ35}lj|WRKo9lvg;U z^j}(EjlHtYCV_#!j>(E7CzSVq-;7aBJEd#T4E7|2alCiA*lA^guyPX0csKu=Q{*`} z3a*TB;HO(rMdXWkCGWC_bf4eqzQaVZIQ`q@#bvZ0`5Ardq~|Fv-EbzZML6e zwq?wY5CL?UHy6D~0`Lze@)go7^J+i;=KbVM|7j%V#YhQfUFWH&qUf=8cUos1a4 zpPWF4TSpHNKR0v_DY8lES|74~z^MJHs5;3XpPy)!vragN) zDf6yF9NsoLqv34JfWdSL38NO_>0*C3A$!)j--YqyyP9T3Q}JV)3NvnSD{j)u7D>0$ zKI+Dnk^k=gaH9DnIt%~NB5u1LMJ_JbYY_fTj!9E2?6Z3(Skfg z=7I&l5}jI=J3n8u3yu$C(OT6zL8c~7PPpxUz|+8^>4NURxTu*ND$U#t{e^GJrd82& z964vo$3>OG*@!raq9_98c*_f3`E~Y}2N$k@yeafOy#si++O(DRH*9lqvJ^Rb(oyqH zP?xl~iBJaa=Mx#sLvJ}ZJ9=7e=)%_NU`EN?4ok*xK&GxYkxf-aAO z19J0od8(O_ZjU_OG%{_))M)^Xr=6s*>IbnoBUvI$?cru|5(-YGgJ+7UkBTQK!!E1J z4D1Qn$%3Xx6%NXa#@w~COasHZ*%oCldv36yBCfvqi&qVMRXj)np`4bATjdUfB<%*X zL?>fl{jkd=U~BKAxSX=|3GEDS904cJcLKcjB7Z}VD!IJMH;x{1P^5sNru!i6OUd|Q znmNfwT{p$YndVDP4(acqOW4bC&`rS2zWTvm`=a>6Gkx8N2AK7Xx`Tk`N8cK<73@(A zUcunmW^&dC`6u7@{=%^M{U^|~ym;CCa@W&O@eBfQgiq#q+3)7k@C4c#RAJf+S@pYc zTtrcbMWGDtcR#s{WvpJ`0v$qyVL7Qkxm4b9hemG1T*`KoE- zh;V7#3a67#4i&rvgT$NCyZ_3m0N580J>Qyb4Fdl z6;c(4ac4U9Crv|le#D7QEL58}F0ijeQ9$Rr9`hD z|E_~Vm0L^P=~3KUM^Tnp4QArDg-lre0|@;Gz+mnvHwYH(5&;(uo3gHlXid?sH(z`x zX%YD5_H8}aroVbQmq3r@cx{beFx)AB~*)iTM#Q&kj2g5xxI3$ zY0_qOc9!Q6Xd!T=@7+n7FxVDqg%%W|Js|d1^D7g)Ez5wW4%u~WGjj6_%=~I_$+rPK z)FIrDE2@1=%%;sGKn{=lgU7q6a;9A}S9Svo<0@!}^N#H#u`Nz}Dyu`oDoC zFUmrIg^M1pBW@qAJVe7Dc`?`Zh1}ieAQ2u8xChY;(rg$nyK>3){0_W})em~Ulx2XI zm!dZqvsQGCGPPw{KHIAW(o??QqH!8AD@t-J*o$BE^mk9*&vurma1-b^T%)YxF$L`9 z1Z97o-6l>A@4wUNRI^U&8Lbgt&(>&Cj(^G`s)vj1(lH!(K8)(M9=deTPJO-e(16t| zUygtooGPV;h-q=N*p(n2l#D}M7L8q!Ke_(_tkVbONj149$KOX&TC!#d$n-i(BM7K2 z)fe;*$~@MQ!cHY~Wv&(7f&f8Ys+Q`qI=l=*(V)3P_eAaJvVgg&`*a^KJ*H?H<+Ga# z(=^UQBW3YvmsBpWN+K~@GKxlSPz!)w`W__ zuo_ul$?EkblUt$a>fhNNi6jet@@Kdk$2ZePYhQT3==UOmhR!i01o(+wr%kAT_7?s| zU{Z__XF2&0%Z=9STdyv_|02t4!~?2E9?kvBse+}=L8W8kQPKRCnzI%PH-B%#f_Gq* z8Cz}QblMDhWQUf}*VRJ~41OK215>34l}b<>sCd28vJ#MMRd~jIyit4A9S!mT#rF9R z1Zn>9U%l$CA6VV+m^KgyZHSd2J@!kN6L};{;U$aB8dMkw%DD5~e=GDxyt|g!s?mQusgOB(BX4Feeb0-iKsFaIBO_QdmBNOspNT``hF$H|c< zH{qw{S{MG70N2-daYn?~*RarR@$0Pt;)2(y=1EO#DDeSo$jD~CPS!c4jYIFPY>HB} zG25j&kkx+yB+!h`Q)i(qRb1WKNo8YxuAEd@vGh0dN*%2|evkoHpH#rnN{@xg*8tcm z=_8|Jd2Z*vlz#FL*cwfSY;Zr*>dN}%ikG!CfghH-nEz7ltSP>tq3eF@f3)=)nkA~0 za()x*Mn#C2;jYY^QI~^waum5K|6s-Y!9cn7$_Y**$bwp#*BF53StNQEd!Ryube5T` zukR1*G}mRcIP71BNpD472KAAbR_<5j8`}KYm&C!G6ekz1KfwTmb;k9V)v zji0s57;LHadgnXRfg1-88dC!7Sy#FnlUpl-EqINF9N}S;ACv3rROs0<_=sdVa;ULs zP0qZEqx?y^-Gbac#N*IoT~r)1dF8L<1a_dd#Csw>P+6n;L$-F$U4{6pndL2#agG&_ zxX{i9h~kJt<>}497A9)`XM5hxc#3Z$%E91>10ho{>Wd8oQB7@@yoA9kogvj>rJ&3A z6h6^B^4IqPzXFW2@A*pd8lO~pMW|DqW4|sBP=*j{_dv_5^E23$Iu*j+JYB%jH>xQ0 zK`F2GK*RRj&#Sf|p}!oHGH+Aq*fEmidi*M4ajc3l3#rrz_-gY?jcnZAo1&7Ux)-^s zJnmm=t`Fi?kG{tj?iBv;Vb7NOu=km!K7`R{z+4CPF^tnPspbth?k11$wXT`J;KMj3 zP}T&ADly8VIIS(*RHO~EPu=`3A4O-uQ8|gu^7~^3p`6p9R(emeN3vjwKDbMU2<|A| zcB|45sl@OB1(hwKx$dG3>IT1_7aPrjbjI``7}7eXNY2})Ed|$G!qJpj$auot?=5-> z1}Vf`i$E*9I+feg4Kg&9RDFMN$u#404<-XfLWlz*lgLV z=a_Wu_gRpGzV1oRgQg_{oB0W8X!DJ{etP#*5VIA4bR`gW+lZhL9U7>Fu}c>&?w=bI z)`;@v*P*;~N@8(81B{_%Vg2@Q@5ZB1nSfE;&Hi04*A|Lj()%3$ZQ$ji9ty(I-wwQ4 z0dTw05qstx->7MCZfH(MP+;Y!Z5b{Cak^tNf8>|gJIa5_OVK$eE`R%8f6>^0u`_GI zoUn@lqHf8St8_+IpF!v-5*I9QfC)r8Wd5|>sGvKrorY`}C7a5KcP%Q#awj|9r6$TP zev$5?H2JG>MMJo+f0ZC#G{z1jJDBiz_c2QZY|mH~BrDk+8#9q)2MTO;d*(vn$5m+(K}yCqd-4dU;Qaf-`Y4EL=a%8=b}X zP>b`b>CgScZc=W!-OuDrUoGLJdwp{yxW(^N?emZhboL|@NGS5W81-rO2Tn28r+;H? zQBAemg?ND8i2A}+kASZLIAg5xn^Eb9lR6B)Yc5yu(E-%@n=^CSj$#i(&W9_JMk-OFVNWhQZI+BUuG^zI0 zXI(OkXJwyReYU_ezbSLG4jQc3^yVAT3KiG#UWzeJ>Z^2Rm}i%aSy7n%Rs&d;9B2Wr zPc}U$Cl1FdC_xVtt%<(t%)$Q-8JZI~mi(kE31v(a?;;9ip^f)4r!_y07O?z48IMUU zciEeUm-$D=#e>6sfTB^bj`)Y<(xrht-B^B7zM}{md-{wRI0xk6p?y17D|b^FxAeX~ zDW1gfC8lGcj%X;a*Qm%;dT$z`e#?cC3MsO}Y`q0dwIHlL(YzRR7@+#F3!?~{P}61K zSbleP(-MBl7{w%bN~z7)GC)hlHi5=fOnH(2T^v-lN%b1381qdpwffI*37yp=o6%(_ z{9RCq1Mo1TIuDkjLKawuHaY2pPryUQetDz?zvO1$zO>MNzD-mz{cP?xeEpl;Dhdn( z;^C-<6x7wX>v()W=yhU0{y9u1nP`QYIsY#3&(?$=7qz^Ztf>$L`Uzgw0RH$6eYtE^ z_8lIyBNwRfMM4#o+t95~KSBDOI<*qIU#!fU45(mmSsXwOu{#FWvc(r;;$q+v@EhcK zrCsS@e%s6z?a`lUUCK6}bUh3^>PYz9NWf6F9q7CH1JDwPj!>i#biXtrDcp#n`TXFp z=pa^ABgQlKP%KJr=w%i(>MUZl5*>bvUoTiv>GS-C3Ric@NPK#tuw@9$psqihfUWzY zgpPfOP#O(TOkb##&`0ik*!1OBeN&lWK2dz%yQ#{zmUCKNsMI>iTN&tpH-U1>SI^Bs zGZTg_)ST7zQ}hkfRq9?$O<609wRNe>!}f!kpHPL_-tN$vMPAVrrSzt~lsDb?Vn8+< zgzvx&Y(Z9Dud(ny<|TB}(?-U0jAP2mx*Q^(&2~*RPHLt84PzEfo&L!Ci}`hD}iw}lb8Psr-c9A_r5c}dq{Zdeg)=AY>HngS6rpS>>rk@YvK7^JB2z0cYZI*(q8Eg{hg=!f?tVeU5BE`ccvahyzEZ7 zw~~dKJ*RtISRJ*#3k~PfumY&PPK}<3+^fBB+jpO>67SN1?cK?5$XW>LhHLkeeW%v!G6*CC0*xDVM*-pm?DWUF%JZXHTGu4exyJj~V9S zgdfeL`4<^1Wz{Ek>sXlg(@skAhyJdt&mtX1%tnw2G7MX7lAHKHb{5US24IcBv>Pi8 ziVjmYOE)VHA!>zP)FMwLouKn?>cn>a_r)&_&0x8U3yOpQf%4=wyU70j^Iii-yi0Dt z)d2AxRBihS**?iY9WZBnX_dO_iKCXT(U7*EJCi%jdA!kf=>{z0XkzMf;G$KR>dLH$ zq`VB)wr^pqu-y4G0i7xWmiLrlZzXkiV5`f;n0zE)b6jjPF>CdvB05DjSyW6tjw44} zMY&6x$atl|@hjP)CB~vMFt6dlQUil5-QUd`^+CV1#u0-K4Qa0;fGRJEYdDsGPQrtpENI7@D0T3iRI*=`zvK2upw<@MGIcj7z$1$ zO9stq{2%PSRZv`C+x7{800|a?JHbM5cPEVox8M*gNYl7G0fGkz5Zs-HAWh>0cL+{! z_r~4+v-3=SHSg5aJ9F?o^G(&%bI?T}Y`2I8K{5sI*A*R;h-5k$cy-oVolTctjYou<*Z4!1Eth$QZ;SPm7C#lvjIVHgu~ec; zt4kqcy2OHor=*3o88bq+e>mp5AmUs%PhD{+C1v)s{WkUvVAB~q#PK!W_<5&^ZD(7% z+3((A*C*l+>M3|;4p<>kyu%Wudr4(y+%p-00tL;%t7GpP>msE`>MNBz3jv2g^5p=f0Wjt&^#&LoR2 zUw4pY<-1gNq$|Hwnu0o*C5R|BTIp?li>c-P5LwN+nfc{tY2etv_~psvXo{QIa4S^*sQwrB+#58vw#x4x_912|K@aS6dGUyB-Qb*d=#Ev z1S@?MuBSb7Pq9L(UKV^Q;xhbaFKs3KL2AuNxRa9qSS?(kP51oBm?@B_)0srJJ3hV_ z&1+M);$J@(KwloFHsQ(q{UE`Zw3;+7dXctj&>zFjUoL+i^-8Go$oxh}C1#D~fW~(Z zY#{u+E$OV5dbzLN>Au}#heQl$%^rfZRzSP9S~VRbVlebD`G~he^z?FCV}3(5L5a>X zAg7~X=|inx0&%Pmu`~OyAfwdu_h_vbt75WXqBG2NA3+#VFw5sk1Ks9ktSDv?-LA}@Jg z96~s9eBx}Z(CBmvA=K{mzV0l<{Q2f=GW?qq#EZLxGgmr>yR4yK2rHi^E1cp^o1h&? zf>t%+LdV2II)y`1C;2GFCwkd5=C2G&p?^<7H5^a9-luL#-v?sGB~qBT+2x+-BzooV zd8pf}I?M_X!-=gAw<0;x-}p7sQ1QfdHog42*+0WQ0IVT+#tuc_RqPQhQ#Jk+b{owt zVwmEV5*?$^pdTftm5i3bE`N9{8s}ZR2Eka~BdbsGauTb{Qf@tIz&oODZOCNod?voy zg_VG-BD4X|3oX7}d7_|dq8N*)%TzJ)ke_jtvg8|yR7P1?8XTvOAF8DnE>AL55BRc) zKk?Ho?&ZOC)H>#8obPPD((#X#a%sv}F}Q>qUwI1Z@%2&>aj9FH#eP#}UT z;zcQGd-;-SzXb{m%1}GTmNe0A$V<PgomFc#E-AS(zh^{S8rT&EXDNg zmQi=sk8*$96Thbjx7-02v07+o=2lB6c~x`xpy)*4Oo9w zoA5v^O})uy?<^P99O&7ler;|_8s+CzQ7b(q&zD2CjO9zw$-=R zhk01)i8koJ)qSI~7*79+JZHU*gBRhwVf5QA2vQgo_3?=92>LY3IIW=1(?~ry^onj* z+a)WnX`V?xz*y%YXy5BH`^zPiS(*P{RoJcPGP5^F|HZcFMgmJ}bO;V!WX+nym)QK} ziW`$o;wBYn;j-Zv_-zuzmmSq7{Mg~G!q33C2F@<}kjYrjBc|NXxSGS}Cn|8Td0vM_ zlPmFzEgnq1$|_dNM6mg7GTj0y2g&Cm=LJ*A!|p#wt#|7G`RD&M*nc|7|8-z!CV*T* zg?N$n|4mz9=B0oNYQ?ak|Gm7h&vT=<+EbjUp=BHvHN^XMh;2H@A z3r>Z8v6YeMxL@0ZxO@fbrw{W^(Nc+W$rRx~_4-);R9^J`h@lN?puobPX_6rFT<+_< z_U21k$}046EAt9oV$IbfO(l4Z!73y~vM=T+R9ydf$cBJuR^+wp7~UijWaCmBpA3^9 zZ#5~4FHhWYSHKx1!-NPZHl02B7{a^D?E*rR+c;*yn=0YPk3Q!_bD5tXhTv!f%3O~N+TCQmm}Fxlcp z_>|&R0s_Jtyuuc|f@(;0mk~<_?mG{QyNr#HtnbKk>W0rAG$&+}pv$U7cKqj`4=7UH zHGV}2_wIzF_)oMd+%{{QQ2HuIDZtvIqym>#^!21lT51>%+`<&J9pxPt1{1oX)i)je$Rf$Vqu{R_3n@I>k3Eyt&Ji8C>r$a9Hs6Fe9-{=s~2w$BGVH$G8{weHYzRaLe_7fde!}H*c zoV$$QNf&1gV)%Y_Pi-5syLq+Q`qj$*Y=lL08p%tQ32fm%rh^lkN%rMPN02^`>rEc$ zaN5;3iPqwk7)hV^5cP`0V_>;NV_CTI3=?dW z!sZM;5FQt-v7qbH80Og$73VIp9-|C zFZZ-(wP-X@O7H#f3DXjrF2U&+J*gA$=DF@9x!&VF;oq5d8y^K-- zt8|d(9jf&=RT*u6I$IB6jv&A?s?&rK@Z~CqRnD!CUqO8pz)Y3)Q}L*Ocem1~YndrT zw>_a#iRB)1D#$^HrGFZ$8b9U$7Du`o63os0+!d=stM(NER)UY(#iGKZK1;^QKS+^z zvsTf6Y`k+Ox3Ns8=p%?;1Rk~XpWuEPL`E1YLi*bBUjWwN?7ZWEs9vtgd1u#wO*PyJ25u5bIR6 zz4#`~&-D%TmI!9iz*{-6ThZ#Go$}$MZ_HcusVr%KCBEn+lxAW#l=BgV9NVk;?IfIB z%WVt`)@BQ%%IFI1r0`E9ul&D9@G=344b>9kKa|{*ybG+eDbmlb@oLLrqW}D|+}XM; zN?MAH_R5ThgItomW1+q&_3^OK`a*NgSS!yg`2vmHr0-}NJn>T*rqf_t<0;&5x>>#= zn4JQh?iqr{^7GjotCA2ZPH|+F6|lz=i@M?lG%a)*vj}^tTMLDPd%~*uZLUB`V@Dyn zZ4re&xV{Ay9SkCZa9q^$a^K$d%hiH>bu+8W%CMlIk|Ss|-nr#HeEy$Sa;Zy*K)qL6T6@_!Ak;5 z_qlAI!b=djHG3btZi!k`Ir+`8m{bi1=S7uF(R2jRd$>99Y^y=yxvt5oMIiqxhxq)8 zk~|Yuaiv#_tMNf9T+ZXfFCFY;YV#V6WUsq9J%dL($ur*$HR~0~`Y5~rDL0^B=fB-P z=u?~oJ1@_R$s0T9@$M!o^c$i-cWFdZMGY1@;*z6-_3nwW0a=o@7;hG|o$hKG*Q*WH zDTZVbi>B|g5%+cWmqR$RlfM9|qLRxxrYk2~X6sBY{qLy2g-F*TcpTiBd~EL z>PA$RniCka2wLA$Iln`W{O-Q#U>FiIrbv`?Axy?pU>^Dwb=4ZB6^RX?pow!%@R4^| zOe&a{Qr|kaM=uNqEa}{DEGbZ3{{D>&@>+_EBkR*SB@pgu%jJtnE@5l?L-1mAUUq=$ z_s*NR;qC+!bJ1^H#u3ufZ-EWDa2+u`LW4+0>hMFc=^b#Lz*se91CnYAFZd=>zn@=#dE58Tc(!7+*i(yUL z{^BXTcu9P9kkvs`i#}VK;i7@@2(n-0%un84u8?!?N&^U`BrkFy$a;5vG#$D?9z%P# zd`9EgON?Cyv^j1yfJB0;H%|CUxOshw7aR6@Z^y)-z)GC)%4bpm{wbHrJ+CR9K$i_Y z^7jZu${X4mG8bdz4))k8SYnlVe6%$mV``*Y5}(5nOsRwsWJDyoCU!=?$NAXSAUk3CRm=*5b4_Tjl=SrA^s z4nq)K)~{?^mxEHm2ls1!f!}iU%kMp2)80BftYUGQWjM+|i`C(nG}UU&j7!{ z*qe&9W!sJXLUHVnmdQXkw3qdc$41{iQD509zytq-S*@gdUIx+a4^!9U#QB`s*{aPG z_iPX3SU;636J}~5&VpkH#b{U1TexDeiqaYH75>3vfR6AT3*mzd7(EPpnf~Ql7j3Ve zxzTW8zoGeD_C%&rCA28fqXfWDFUZ&naQDBe#nqng_O~E2j1E;%Esn1SSu$4987K){ z=@jrI=0fpZbFS6ft+Yno(+zMjGp#d@ihb1qtG4;|&??0A4&RlS`LbznYD&L0xTm6bxp}kYU~C<=B#L z&O8xj+F-1ylOWi=n;L;wyT86gGag+dBmZ*OyDG2|tw!hYU-$P#FhCmQpM?D{^%B#w zX@Y+bJCx9&eFnGCE3O9@(=)w$zBm3c&DHCIg=c5PJb<@4Dk4b4<4|@nBm~GK+D%se zy;ep}tE>OqB*LHaG%m`QB#oiW(;KZZMttsA^cP)u*fUmH=XMT9Y!`n4RYHO2Ckob; zxu2dvmThM(^~p+~P?Z9^LmgzLer<>yER^JaK0e1uKC50od0?1fx97DJ)6|Kedzx;T z;S;jA^ei}E^gonzKOwjl6i=dG!Q}hVg1t_S^=D-sun#Gd^D$?e8Xd*=u8mL`YfTc3 z-pq0`9spq~x0f3{d=v9{f&&N{=attFVjk;p$oox<+4OizM%^WGBCi6&GzXBfmXlQ1 ziN74TD4pEjzwA6?vCH|n*~}TM_G6IC*geCYI@G;R14yzz1Jd}V8~)TMAH=a50Ile{ z>+iaDoP8p$1+mr;`2mbA1Ceila_n+o4~W4ZFx^TX=uR=RbnFn`jKLFjIl8Z8o7t|h zUZa!l{f@l$es^Kl1mAGf=biP*5?kX4y=xD9P%x`7Sll}w0_XE$QzVE}-8MDe`mY^% z-|JKbiVAIr;rDotJXN+)*|B}vK?u;!tXm~;>Q5W@I;yJl6Bkjd0dioGRENEed?Eedo|$NDCxHxkRD$PIRRwYS>8G^1ZrqFt@(?# zWY^&kGtXo;;cbON9C<5j%$$XwfgzlR^;*6qe8Z^jlr(raxr%#Rjgrttn}-ystaHLU zZScwgIkQs+4#=HX!}dNFNP>>;kb>7<*n?Z}?d(LIWpdgqy||$?_ac2sNK_tzg7i*u zF`^t!P&;>lZ1agr13}XSqMuVsUziWIA|x@^ejv$WP*7o1jz&Us-5H9)v*&IY?{GsE z){?UZ_%MF-$Gwh6Ta!o&A?Y_pqQMcdg1fb{sGm{?yJ7Qa7yu#qkjK}@ES)s^w<0R* zW%7Gj&s|;BF57>wv6)NAfIsZ*@|CczHfz>F3lPk&gehv?p)XK)r;VP@M7dObHZiaD zguhr%T1)*y*8nore`|>vY6>_#%NSu3h)LPxcHf|PCIXgfvk^)2ZtPCS2X-PJ1+aNS%aeK6nU@z1Ht3^=wyx!7}v#^jmi)!_GBR=li9- zlt%cV$p#( z#*K-KojKUftK!#rYaM?)*O$6P9vTcz>6i_=9HjH0wBy~9J#+EW_yw`EZ7cqZow=r1ZPCuopr^h$MJ)0I$b{y_e$MWLv4J;{3Q%+k( z`U&qHCVohMJf__6urd13W~y8W?6+PF0Mmd|Vr)r+mZLtY?!4>dO_Wl*Y_yCWLH}xE zL&`$5)6+l&_5W?}GRh>_5FpC_l&{@)RNcGWS30nPZNytu^ZM!)-D7U5`&MBW7D|Ro zAL%7Qia0Kw5+I%Twkn{hjUCCK(u`(w+;^|XeM?7iI^yoCc@<-?3r|?M8xM%rRf69$ zW}{Lm*F7gj6R}(%%LY*;MXW=~o{lvq`1}YF0d>4H-!U9gli%gXOIot#B#FL5M=w^f zE6l5PINBCy9Ptda)4ZX5BC3ajt+pBA@(80WUT(^qBhwczaTI>-dP1R8J-j1~gPr(4 z5T^13S!-Iz4`oXP9_Pg7B8C$*GhhwO^r#y=6HhylMsP+3Uhw!&dY8#U3yv?FHLzqh za-(SkeQL@`+WNlIECx@vzYlk=&_T##p&KyWQ-?or6@T_1Cf4cny2SkUyk~1_gmoh5 zH<1|91qsRo97t>mwxYu$VI4|*u`W;&vboKFfZDm7=z$_Hal z7v#T|;YODw9iut%B20D5`BCTN$I~@yE)9!lByvAZz)#Rdc~+(%G>Yw@E=kfxbuyZs z0~}qZDKl)%OuBeDu_*C&yDkoUV`U%*io8tkzVmIWj1tRimaS=i^Lzkh%gy{ZRVR$8 zC|y~+D{9NPfBm?HT5Pnsd@u!%A!e&uJ|$sX(dc7_Ik^(M_a$R-^Mu<%bMrwxSp3D- zg&P*5dH%yY96z-phDXwKrhML%k;_4D`9?1nz_YkPhh1uT6x?_>%;>>hm{^-`9Zg6l6&V~>}YT49HWU^7fHKO#Srwv#Xrn5>Q1ymv;_dCDuMDA(@L_cX_nZB8+Oa6NH^V08`-J_CP!|UECz6^Eg zKO*@_l$i#_l8R5VO~UU=d5^Rh*G@9R;?^Ej6+)Df(Q4DqFlFm9sbsZi+Exb_6jrb3 zGA;>*a9!{vc0xc$I?DN-Rp_r+S zqA5x;TPgBmw32V)LMpu#?HlHPnb$ncZ(4m^45%0;x2AbnnpxaI$(An*2Qa6o9B~V6 z#+5RU0sB6DgvE+L=5PSMO)UTrR_oZ}A4rf3(_x_)6wZJ3qT`M#lZ9*u1fO6PmAq@( zs}~0Es+rf{nn$?X(;dYsh&c`L&5bK47Y+(ef7j)ZNk73Z>=u4$WQ#TC=jpqrmx3Do z{6p*ta}`y=tOAHY#c-tNExU~n$&5e6b;Vm3bv+JbIbIlC(o9Vu|l^ViMXNdGe$WIcE1tVDXrVY26Yet(*JmA&9oojPUMzC z17@fTk@WNaJs86OGw=+$-7Z!Y^JbzTlH;d>19W@!Snp%oZ?E2oSFxG$MTtIyBtpwe zI0=zk7h?pebx?iKM^VXaEtZkhyG5wNCjHJ$ zDGFnH_@``w{0+J6x?>}d^Y?b`F)X$1uA5l{5vct?fI$8i!65&qf9Lv*Se!Q5tB@?_?J$>=)_D~k>uP4BdL zC+)Nh8Q035n*Log3ip!N-zgy@@5Xtb?U^6@yH8wfXQ~bs@jt$PDSv|{KYAE;i5RiF zNl{JF8ZW8~innl=#!NsT-0#Z~3$;~O$^-&ViGw4C=|taU4{lM_qLH$XAS^0P`&4>Q zt54O#`DI*48PvIZ09B~Yz>l`88@IeN-MnZy6ioz@ESO@|DZaazI7^Xe6hrWR-Y`E# z59E9y)Q&W7D3en-lQVoh8$Ozp>L~w9s~>SzcDk?Z`d$6~*6`8$t&z8X1(gc_tILo7 zU-`V;N}KBadBYiJLUo$8$fJ2br+D4n94uwbu4Heq?(<(?Ex$cf5R>DhY#oe!*lr^K zEA&*G`d2RuWcwfU;s4}cWW~!2MgI4r{Fl#iRyZE+2X3iPpo&Z=9KP-6$8R?SI%9wm zqOq3j_!E*;#LxWY9+>`f^?%yrKmFuCqv8K|GX%2_qszM$e42%+$Yu}{ zf%?Da#Vg3o?eY!z;rxkUKQv(DNc9^37N!i8dGy=PdKW=ANO>uF)Dsoqb+xfa+uJ1h z3D!20NqD|JmPlUq3e2i>B!yF=MreW7wTE7M<`p(@swtaek8N=wOUCSp-!Q-$^2m5*W`Yj0bto*LF@lq`M1ft)(J4Ms z>(n>p?kMVt7O7-Ht9IHNlk{*x&L(YVXY(m||B7A)yDkAd4^77MzuU3>Ngn2;9MVVRyv%Cy zXFkmUaJ;czprpg|+V63He7vDIiFITHoRgbyQh*YK@AaoW(buQa_4>i{s=W0x z*M||Cs;p*Fv=8_HAO)Q$2D)e9s~OzD_zX;NO=NINi}m+)KY#P8mh_*9!E9I*;Cg@S zeewq-n&LO?LE@EnfKlvVWCu{*5rJ=%Ig*@Sf-S*LH${(E!9xfx{=Cv&>8Ob0)KS7W zMo3AmSMxLb{WPP&z{E1<5BZiqtaWBkY(SNO0x^;W zJ6(59Yzg(+d%x0_I-i1Q*3+2A-q}wW%f(pe`fQVT2T!X1auNiT#~W@Oq>Fn`@1|Y% zcqyfQSg^$)Y1?RDlF^4p3HnJwwh$E7Lj5SHV;pg=f=5TD=LHZt<)A8DIa5(V4l}~DJ?P95Z@}r zT#+xDCQffBTpwj!pp^T*c5$SHUe)23|C0o}tOe@DTL}6rWv$F_qGBVEqvOr?G)$>g za;f$+*WRn;GK`va+2r?hBunkg#E_CceIt>cYdOPH)+SiyQF^gWlUwV=g3(ux=+r1q zu3Eq(+%#j;m9j6d`3V|uhI>OD(>{E(TZ!4V%-LLX5}WU_-ftnCmC6mB(%BWjd!rL) zyNPnvUw)}+!0A~qQqffy@X5B+`#0Ng*|IF#hPB)C{_i1!RK!jl6mGw!GlTC8_n0cB z5BhYjXz%Y;<)#m_W)={k3?DzXkayNa)yR?DACH$ zGX(}6OUcpw9K?r=Z=6HH3Oz_}T3Y-rd_5JK3J|v1rZCPCxHq*-U3_*fZOd^l74r;l z{)WWQ2y1>Wy(sM%+F>-cj_e&e55d*MQNiBN-p{5k%2>Y5^0mrzN#L$%`6$b%w*?^s z65ys`?j?(}!M@&r5V59nz1cRN_MfN9!!Embb@dSiecm%_1=tLY=L|T*%T|+}RfCm( z`cR;_a*zYN;AxY)<-Qzs!`S?dCE; z|Gwho!UArESIsGdXPXwz(MULk6d~71wtqRw)_F$zY9qzA9xsY|D6U-$dP8Yr&cn7k zAWhB$_ zEH)1!^xpi?&+ITXc=EsmdfJ-*7+vK=4R^^R!Dh_{x*%x*x(CS8)5Rd%cyf6ggPko+ z2*4z!g9UP&GMw{gSZvhysMmF>0{f;C@mQxutBY&x}tdw?s{3A5J*{! zI^A_jtr|O0dBodljA;cE_^m*%zP`SsCnFyyM8o9viG&5bi6TkaN|6muW6xN`Va)R*cIds?Nc@72tr!%S+$41@R9v@rVz8LHg1@)-Tj z(S6x3iqOv;uhr=-%4MAwq_7J74t*vdN&I)%EOjx1EMLbTJlK|F)^3mLu$3W~p3&bH zq)oxUXq!3B*`KX7EJ|&B!$Ag)t-sXw9To*J9+*z`CdHIz3k;u~U$-6kT=v-(rTMl7 z6y7EJaR6xsN^HV`xtsOTqoZZ6Vq10nF(1Zk%FPIpucM(I#uRNYWV^t9t$GtJ8cj8Z z%4Yfd33I(QB$Zd)bd5}o?@b5V6L2t@&?#-a;{FzM;u@Sj%;DgjWDYg?gz2GH-RMSFe(v!dqyNpzZv)|o5K@n zhnG$AprjJL$DcP#h-gKouFWz^8&=&gMM!)0s%|`j?l#>^#K1ut1N9QsY0i##@}ogT z@*Q|PpN@ZpajKWZQAHtsTykS36et_1%-*K7H_mIsXz{Xkb}g8^SW-OOP>WPZ#^{zm zWp%*2hQv#~iRCPeqe^Nlx8uJvT26gFp@^0%R38gM`6S+JO0pA;pfRO#k|cB0WhuDX zP@H)K1B)6qHnt=T5SwCEd^PNsl5oty!gU<#<4el%96dTvJK6do?4Dn;U@Pp@0+yD7 zkUQ=+r=kQRE%mFZ(tQ?P)!%yr2lP{A&t}FxMFI*jWY2l9Y@aNAhV>HJ#eY(c@F8CI z=NK(9gQU))xV9hzlAs`C6PrnQwGAMMio?nF@$(UtU4=G&#n`|is6MB38Q5)(gHg70 znR`cWx~dYH*b*{?J2u%Q@ZbfDxlUMuura!a`%$PnW8oe z<~(%^0ti&`Vf$M+q*r8`1M4PVt8wCzr96{bvn7*HDay6(t3kz2*@n()Ze|MaFriD{ zfUYN#?V!C!Ky;lb63LQOdkV{ZxceaN7nbjO7XzQ4{nVRB67t2Iaff97qXiZ~K(p}Y z$<{s<2+Ny5aO9337d4=2CiZFWVMx6xU~FVAou+z)!({+q=%j*~I3 zWkSsB?dcV)*j8LfSZTqy3p?uv)d<$$@7}rY_*8Ni9vs}Cm1Pn{b7HWTbrxviKBSO| zx$$**6On|#(jb-h289|-;Tky6F}(n$aC@Mr`>o|hx=4As>(l5|9xUrhSzKg5g9|Se zGq^>__jOFzLY#MLzy@$~^Tj}CrVfg(*-!_AC9E&6t1{3wVlGJ~dK%v{b0hbvAnQ(S zC?54eK+c%!p-Ky+Fe$&ne47;FTK(Z~HO#W(TYtuX1fy6yr+=m5C`qt#qk3po*OIL^ z>1KdS+XEwrT^GDIlx$cWfc5teK0sk%YdsQwwT+Anl`AtV+p)coo#Va=M8%~e#RF!3 zX71k@1`8*0&S^hMTFxOBO+$hO*K3a-v?2LRo?!-!97mt3lV^A!6kM5MM!D}j-=s4H zi8Q(A5EUDMoOy2cC;qVO94Jk{0Vv{_R<%Iq91*yydD*X3Dnz=}i{qrZa5Flnkg)6n zPyo98(_E9!${>-l(fx&yjT(ir&ISJ<2@|!?7T@LoOyp)tF~V*;f8m5#^*Y$Zg#kjc zzGU9wo{XT2mC+>8H!36AT#$m_(J3>Au6umHX#^2klEYw{YQ zKE3+>QR2DS!vd-d8SWFDF`78m@%b@YwMaNyLadUc6w@MO#3o%;pZPJ@ph^yTiM&CZ z@D-d+bQ|DH^r4=}ayuiz&~xX;KQ7bEu8&u`9++EudAS78flzfixb@T;2i(=rkT=tY z#zkGM%MERI(;SNJ2rm*kP-kC|l_qocY|sdq%J%J$t4s!a_5;Qaj(32(nSPR$Ps3ND z3>08z#`f@Pk>uHE{5R4dJryL@f{?I|Rs0?!pbvHgR)^f9>!fHk1}?&)jio%z(H*YL z*@y9eqtV|v{@r_-oS9xcwMvXo8W92-c7e1m{ZlOc!sL9h(4s=Zro|otW9>2p0zn)P zG)U$EgA87lBbPy&z$xKuIQZfZ>qY4$QFCkLbTV;_Vwm8QfO_IWz4to6`+#pi&T*bh zaskkbr>Ki~dZlL!pR$Bzib_q^A}MVwTe1Q@S7OW2^ENAb#IoEZA*s$~hlqYO-v1fF5zNCo1ECeH0G4>W8pF5gm;&8&;qsV}8SO*}&2Nh`4$bjXrx+3V5}YhyJAylD;{lXJqz^yTHI}mDCb9X?D=oxNw-VOl~V2yx4ViShAXoH{lKzm@PuzijZn8` zg`s?g;AB7MR*R2a1@)y|!!OBQ;0~T61}E7OpHBH|jr8`ef9g@0HDvVEnZU{WJIN+m@Kfu znH4&)gtK5NB{w{pEyCZ7+(|S)!sYomS)DC9zq8>UOSmg$_$~m1#U26!C7ip(jiZOx ziTVd>AfGL{lEwMa{K2vBGk;+xA; zHAx%*wf}64?Y%rY1dXO|=Y?}SD0~};kfo=iGb2H(gNjnKeZM2@ zN$O3t=Fgb)ss?E1TKM!0i$q*+`&XE{EL%G&0jIZglv$V18%SQd$fJ+uS*01QsqypS zr^bjp&sD6^6&BMH?twQ*LA~>xH`#?N6=@G-6xUGk=Fjm%wrTI-7)QvGI&1Q=tkGyI zS!l8>)4TcbZ^u@DI+afD7H`tPdib(Q4wkWA6ZeV+sY&D70dDLkYOnH3>?z_S+uCq@ zvy$5#?wbatue~HL=7zR5JlX{O1rbJ;e>KZMB=ht&-3D@C)vn{{P24r)N3E6MNi6GQ zPCwC$DCAgpHPEnuAiqVb?iDAzE} zvK8aj;TdICwTJ}S`TffF<$0X7#8?qsyVrqmR(q%HbJB){=d zq(h_wgYOzOur?__$T@)W{W{r#??F-%r!k{(ge^R3AVR35`tF-B>Iw@)WR9s18U1bK z#H26B6-ylU(A5ZFC_jU19>+rEz7_58m&8)O2PoHdl|b4{$NzGFWC<|wxFx~=tSfPn zl4-SkMF3A5XJdz2!r3jftSy<>dM8tHoT1saJK#3rz2Bd&h-i;AuZ(06SvJNNQAb~K zC0vHIRBw<3tEBQ0CLsp~Pv+jok2aA0br?13qLHC3C#alNpRv$AB}FG}*0r#AWH8|v zS-YNHO2bQ#P84#`Bym-7_aQ4F=?b%_xp!pYB(~W*+V5c}D+vk$oU;7`FWa;C9p}Tl z%E>cjOd9K(lBaA>dZA=Aw4@2r>)LnqFN`kTjG_N|CIv{(iNT<0WjuXM@${WZ zq`h0BF7aa|B5*R~bmJ+vH(vCz$Er$5)s9R(s1{ESEu@+VphxWx$Q&!rk8; zq1Q1$ONOj9vO|E(VpFtDm8Gb=zFy1`K23@v>9PV2hL}qh(lK$UD4l;h?nX@};`!2M z7PxbNM#2{Rm0Em3{ISUoS-`d&gKzgjz5Q}0U>KkgRG4|1yl?(pad?=nDWIb>Q;s1Q zj$?tP;I|M5l!x>ox$$LJ+=$#Os^SSOv{WHn>kYj`TB_(1ACJ?8uKHTt>NFizlcKW; z0Uh|l6J`6v!?}ZlITOQ+X0v@9Zk~P$3mso7iY7nlE5Isz>P_1Iv5Eou88})V!-m}R z`P@H9Lm$k6j7*<0NJ-&{cmAm@Aa{rRfjdyunirD4I6xWG`u!iId2hv~a6IpXa^Nez~^R+-tFCFL52KIy?gOSn#<6J+ykj)4F42E=AaN zWggqH z-y5_wgNwHu9zyi9PpT04r`o;Hm!Bs#)P)y*|bL-NK2k1DE)j2lB^Kzmmnr_Q8AoxyqHd?qjtdEDsv2 z-JKA#SxHo-s|PWv-Cf#QcY_S?lLeo~K54k?Mi&iOp5y)qaYr=E(YW-uYiZctHr=+l?dC#9`93CQ%cX%yiN3 zx_YhenVQEHEZWb*NXM4tY5uE&(r$9Mthp7~z_xF9*uXF0w{%g{4}{|RZqT4IR(|~o zC*o)WtB*xrkrL>Rdy%yn6f-uUMr3Hk9&Y)D2f|Qj_Z@h_@EyE{vpfW|uP%hn^x?iX zYwlNNR-_b|)p_1OuI%$&87Ah}!k^9UjV-M!L@&#$`pZ15-&Bus%6gkxEtlsR8Q!FS zm|=zF!F@hk)Tg}z59fW%jQ^@Z=?*{Yli=Jbg#3#evjyoFXwQBS>XTaaoRt5~dt2o@ zSJ!A2Kb8a^do9OIc)!ANx44s)b*b9rk5PL9OB$Ouop7kwsI_yP@n(uWl0WVDkTrit zWBxM#EzF}%qyEXfZCQ@`qzcq9?)&yt`+3_R=VjL{$L=7>7q8pzrFo;N9c{zXPmT}$ z8DP@|ZLl;YpT;8{%}>vV^R%666owKyJfcmJLs2OE0zd-g4NV_ckHx8EoYvxSZDO3P z#d>3&%9MuiL|;D?eiUU0lR_8eqzM{vy9fAFBY7t_Py2KtSewr2LyR1eUD)JRb-gvP5Had>lK>KqA)RUa(Sa}jL7{SqPBOpjuJ(sS}XqBis2UL92TQ>84 z#S67p%KFNh9*Ni23Us%gOTD7Q)(gdafYHakx~Kbf*XcojnoJ+9!Sgy=PZ|DIC`}?x zetck=yqM3d6Nm*^df~f&1rgwi+*X{@u{aNSX~!jbcZ+O=v9!k> z0vcnYYPP36g(IbXW$7+9O;XJLHH5DrC}x(U{F~NQwiBntVFuj+f3_UhlECyfWXor> z2>x?Rr}Ms6XE`;Ojzge8Hwt*jyWhf#=R?`yE)t>kyclle&ui;f?*H&EP0H#^cdVMx ze$7A$bo92SEH6Hd3(r602)CNpY)JSsm*3|c!hxTUrbBg04RuhLnRV2+cfRuoVRbzw zp!_r*ZOfWG!WR7SRhD>|8ey+xk~gjVwgaxObOSzg`v~9D0hyjVehj2!9$1RoA7k1(5-2XOMiSy%vspNc0Z<<$=)_ z0UQ6>0~=%jW%lmKX*LTh>5}x4oK1nIUHHdTjt$#Gx6GAWP`>3d=ChoP;DXm;LL&_~ z9-^nc4tz>Vjp7Spo7KiPPvE*3Cc|wGPUiy3g2X^CbSQv4Gq}5NANWhusk*I=KWPsk zmP%K$Y&}otQ%O{PvH4}04CJ>+fd%eGdT7%2!|%}bcAE*NpP@@v9Ve5rL8_bIA|-tW z`0%aghGA&$^$YbQO7ei4ZQT8YU*1quMfi%8?fbu(bYr2^>BXo}kJf>^<2nZGtPUlm zN&Mg#{<+oM3BD*~+z2`&AQ@9>)^seO>iFFSM|w zee@rs95Kiqq_dCY=~b(rh^AiE#X_r$W`kZ8)%gb=1K~2%k2BP~uaHp66EiMH>^yb< zF3!*ji>Z;V%+?y|$dk3MU?qM;UoZx;&+~md_eiAv@jA%ilBPk+`NIlAjBT{Co2L}OI#y1Y&Pw(pV zShs5H)va_q)jKeoR5`=vanJ}|mYWsd8OS#OgVcp{2lykAKNV97gjh{|kyvPo)}^;e z5w>Wzs^<(>Q1tC$Uyy%soZ!-@SnRQ?giskgVzV}k-FBa3m_QUp4h`bbr+bK0{qSg< zYke6jj5X^6u>OEd4gX&v*~(_LXPpU0_r}M2S*K5|Ety%d2JVOV8zc{Q96BcfS6}9* zvK@fMs?LFRr!O&^rp(o5bZ)J|d&s znQoN??&jt<3N+R8x6jf=ax1S|El;IQ*jLu)pQWFool>6tBmafhrdkcuF2Zl&_prIc z2SDN`kK4DNWV|0|0BYFqTKn3IHu;BLSV=%#Z-tu~Z?Zuij@enNXf=(9ql&nFEYNY5 z!aL#08%%Bo%?q!8mzPBu;A7R4vp*Z*__6#cHU~_xI;#i5R6PLjdtw1qB2U&5Wu7jG z`5qt?oNsBb+lwi-KRbuBssMp(tAzf*R10d#mD567UO&F<6M%6(|JL7W(mI!O>vuN` z{wt;f<^_OY7XVo6$o+5Zy#-Vp-O?^R2^QQ54#9&4cZlH@+yev%&fso=Kya5ZSkS@U zJ;5PBaCeu%T_%4g?|aWV-(B}#|0n0W_xyLgGpl>Ldv@<#ReM*@?AldNk&3Vs)=ZXo z+80?|K5h(bCi-5v6IhZ!YbJ2gJYR0JCFpyU&$EQEU}3jyrZn*-a!ny|C326;i1WL{ zNDpx|@r2#OyFou8(gX8MbE>Irlifz~x$~08Z%*&p_Ge7v24*t2y{tbExpnVf?r7D* z(NAougC(=tcP{HRQ%2cx?^d48ih<+H(xy^j+uS`y5)5TuC@u2t*yPMhRjh7bj_o+^ zfZ1x(pTsINEG_nLO!raG^$DPeqIf49b=MKX5_7?&GYkcU1(j(-se>3J$n*74L;dA~ zTkgQjWIw;K?fepe!o>bH7ol_DPsg$<5$@z0_Cc_`wAW9<`3Wjys$ieD-4fGq>{-0n zQtE=R&Mszv2}@>m^mX+mTrnzNZGJJIqvk?Zd9l-hHkXz3D}oN?{4zE0Fd(O8%u5a| zmeuM~w~-XXc0^+ug0tf4fc1S%#Eg86R}+qd?slhcU-TNnQ=vCsGGFAL%X;}}nMq7< zuKR~eKz}Wkz5XD9^#KK|V=L>AGH0t2%0szvw&K#Wxu7)6t|C3!gNSVE!ro8WTY|7R zI&KPaa#P05B;bYLlePyZ;nz9* z$ofP#=@#(?*cK6^yw5g8>xpejc!i|PdNjXFJ^*BiVF@YCU1b8729|zeuV#;^s4`+} zmk9ZQm(-J}Eoqm$95J!OqprUQZwpg8W9XlRS1b5HF`;e`mz0iKXXDYcWiaZ%p75Ao zc7n)%v=iixHq#bpS?VO_5)~O?-mi9loV93`SE#wtige#Y| z3AGtX#{&h48m9nb7a&RF1HeD~kBu<7bZ_>300!6YrG*Xo=%Z{_)Bz7W?5$CVLK8L5 z_7?|%tZ1ZvQe^Z0P4!Uv9vgQ+63ZJEGjqOT?$26xYgDC>y+=XGT)tmSJ5{UZAlkVU zvTbGK3H;G4V_`}c``S<)oFIcpj^StjmEv|vb@f&pUoS=Ts5aZoeB|I%WmEK<{2KZ4 z8u`Vc9z1YaUsgd*eZot<;DuxUk`C`@qP~w2#D?k5`|eoGYxZZsZ3Q~|{2SNFLK$H- zy<&LgH{+U3&cb1VG7Klt2V|+nwhP%`wwWi3imys&17h1}9a(ThksJOvt`YjA1TcoU1ymo%efS{UBYz#RD7!wrPE`D|A+= zOD31XTc@V+ZpBY8mNVQ7+G4?o2~1@A?x&J2ck)mM`P$r%nrRV~U#iZ;M)Y zes7l#NQ}Et77@Z-OXD<-u|vRjU~u;KXTN|Q9W{O8?=XH_Ryan{NeB~BlCjBIJNeKC5_E9}Y zc^gXljlfzSKk$E|=uALML~|{r4Bb)(V#%k>9>p2zxc<~Lpxz9afyMm50EDlEv(y@u zwbZfdE&N=r>2>m``Rmf}&&8Ppuidi-+8a|Tv5&>#PiImhR5msP18Tbv{rM>xP$JvM zF_-my2GmBJMJaC;9=kxiT)5drtFjXGh~%++$$hHJQ08t7Hc5SaAr;}RVpDWirp|@* z<5J4i<3w{XR6JO+fZ(hovg0&b)0$iqu7wZjSSfGH9!pb7nk-Er8?8woT3ofQ>X2_( zS&Ix~M}z?!r$klaaU$2e-mZbQ%;(CWx7_te0=7aJQ64+3)Nb!CJ3YjbElk;(9ds6? zgp-r~n7;&sBMkFgf5|DanOcC};fjyIYXuERt~PJPiS@THJSOr2ZOlj$>mf@+7CF=u zUksNWZ%A`pkJ0s0+CAQ962#~X((mc$8x)g`5Ex@UMk)0cCU-6^(>W?_vRR#r@ciaF z`E0-)ug4K-x8hYes6C&t{OsNmo(;Wf)JGmA=EY>NdCu)!1S8t&OHh(%M?5~;%#=lA zk@x|%`U%nx6MNXy)G`9`9JCzMUU+hHz?O}BS5M9r8IgK?R+E1c9AybY08J^&@2~AI8T^OG0CUOI&DXPhbA*?n z!&!bjOvN`$e;xk&lmo z`t`cwBWCqwJCYF?e zEgXOP$V1cj$Yqj^ql;2})cTbKRL7F@GxfY|PoqdJ_pT-6GN;Ml3;?fIy?l2&2Lz7N zjst0lmL3j32XplJ5(*3~-=OzR)6!LPBL$|@$d5ovF%Lm|-tVuq zj;*?<2mLr0HP`&~0s}}_WOcCxpX>Je3F5F$2WP;$U0}mht~Thp>=$>w5>w@(?6*Fj z>POx>XI^>APBgo(v81W=ll47msl&M*XnrkGE#-i4sSBk|N3s`VZuw9%5Fv}bZzHZ1 zpd0ky;au-c7NIfcJkQ>Oa=`ls}Z6=c&`} zoK^5L|5Kh;Nmb04s+GKidYesoFFdvBR?evRA+G zBvx6zjs+DVD_|M>Tnsy?&Z`O1{!AmokqR|rjF_fHD9G-D1m;B?qjx}piW~{kK2$Z; z-M;>g$4SdWuU4^)M_7?wpCxp1y>!&lMu+XvBCd4B=qP~@6Z2N?SXPd#4QC$c?d`-8 zFf_ylz;`yQk6j7zja|@q-BF6dEN7Y&nk=TA>o}QT%HJVITQ}B9=4PcfRhsIZH95+c zkdT(HO{iuDO4m9s?XQk^*VPXRYm^_HsA7|8c554p73gD9 zv(}Pr*24sMB6hj2)OR-P^&dqL3iF(KP)13(m+YZbPbme>Xi6y|7 z-7snjF+bCNABxRs5=GV*(@~G%{w*4tHb$Gx6j-bUG~czoqL8UpCe|yvR?Xw z1t}jIVi1?2!1X7VbMnwrZ^n+sjn-_$sT-T68RaT=FMUtvlgs-7Ny49?1xCno4{yTF zR`N0Dvk92#(Vb3E#L9-<`WVtAH=6W8C_gFac)9p5rhv4 zPS412Jtpo}qH_=^5~|H4N`el$+m((;gntC3OOu;K>73jvkPgVVG)Bk-U8=@TTDzDw zE|5`mxZO$U9m{H63*B0p`quhP`_I|mZ^gyWmHRp#ngM?JB@+a*@Fw5ng>&;$bHK6e zCjFCpq4vz8j*i}Uyw&X(G#H~j@i4WaGll(R=~Vhv*sYYxxN}h&-z4fae>jPy!~b( ze*mnKks>QMwV{ZFqsgMq0e(fxp*-;=rEn>G3HObf^+Za%qs!opQwC4z@0#Rz!}xd2 zmn;5MLyd>%Gcv|m?MYO}=j>+nVwHmPO9eeEuM19=3}`G>)-{0y=V!NrI?Q?bNyEl8 zJF2=b-0UmmKI>@ph-|3Gr*qG1@~<{&GmNm(PFQo)=&OkzEJ?Cis>oBNa>iX+MI8#> zd1y#V8Av!%B3B<%#oL#FN8w#gont-GWNo>6VDE@-9s`ljVP(j9JK= zkcw3K_N;g}Hv=rJhSx*!0m}Bk(kWgu-iz6st=Xz{dZ!Me-ko+q=I%Gj20=G?*6f#n zEQPoX(v}9+;Uqj-ge~k1H2eWe!n1!;JU!??T2KBKgvAY_y)K>gR(rw!!#P7v@i1OW zOqMT?C2uVmsDZda#v-=9q{A)KW8MPFE&bU)zth?VJ5xYWf!s$1W$nU(f*c z8ORp=wXRcm`W@CW-66I)ulsu}?>87L`uYy6YHNOjyvfG{zyVJ&{ux%TB4>EHH#4dC zesHW`4~or;t)BbCA3qS|)qK1FXm(7>-+PehRmLm$VnyV%B_!{2A9b`6RFNhwlbFu` z(N-o!pv3Ya`m~#ptS1F;DLvlzOME3q#$?t}{zX??E=3f}nbS4+<+k}BdmV&v0Q`Lz z0DsTS{=NT=_I~vX?VVKQkB?vI@c)|&pxx9&T_nWK)(mzv%5+pCz2Z7!iN$(w@%0k$ zT-TTaGW3`^y?K5t^r)BkWST4T#o7iT_#a5jOVK6BnJAW#koqmYk>C4Ky8jJ)K#MHw zchg_aD+ts@bVfWRM16{g+ZMiz`vdn7@+1H5-^kTYijy;f4mUaYoh*3OcF?(l00CFCYuq(0>> z)^yBKktTi!QgIiFOfEI(p?s9l38!sK4v@ed%?_G%WbE52F1~&H%*Tm3JO~j-$)jh& zi??J~^HTG1>kPMeVDxjyjLT0wbb|ckE7+a}E)90bumFsV}Sq>4QBw6Ak10Y&y;jtu-Ip z3r|Am)lUFmdR<08X=?oO06pZYmHT&EURHUtJEMJ+IW+avjTTwc8&5(+Y6omFf6jC9p3}=;X;GAFBXD^v*S2R|>r3S3B4C_+ ze*eTM+E$7`CeS{X3+q5q{nI#=Y!zQa&ewt$P^CgGseDBQ3G2&8^wE@oZ&EYbHIQ>N z_2#e2beeBzL|eLpYdHm3PQ|X}7`UUK^}((+uwt_I8sBGU@&=`OgY9hgS0)%@tz4@U zXv0}dY`IV$N26ijkH@@rhPU|kd9SdEm(jz)2f}QQT^-$)naq)GfP`8OrJ8GU>SR_xiq7 zmgwQ!LkxTES_Q8$$TngtZ7ZSLGp_rjJDN9PGWKMA-()a_?P%vyGYD zNG#*TISUJ7LT&eyYVnKVFY}TL?uQ@m81rYWH0%Ks)kBJi`@wzwr}P;!TD>M6Rn`dGKbOIgms9vf54tyzrJXE8Z?#^et+wgeAhDmqtDubL_oPsX&z%&G%B z9h3-1xoGkIazC`wML+Yl@|zT)DoF8`k{x5S73*_f+4B}6OJ)eZRzi*5PfAVu+TLG; z)zF?du%a*ZX4LtqY54@;48%_QjVMexwv$Byv)!>;cg5}jxTTL6A|lL^G_~uIt{})D#~v!T}8B*a;x&tE6rDzQcn7@a2QLJ`Uj#EPXr6W~Siz zQNty~Ho9K@wljPSp#dg;R2OpQOE6ut80ZUj#p9I1?Je$oLAwb`H6tO}6)aVfKI+c4 z%5~G>HmMjkEzx_cXEgSd>^^(5S7copNN?*MVe2Ja(>CVBFy_oArmM|nmSSB(XB}50 zyHCPbL2Ve-OmO?A76ot?hZ}_C0gu)jH=9nytdy-Y zNBoKXea-?-w%mu56xVXEY;1g?SK^9e&iZ0jEGdsPrVnHJwOzu&D&b2&?4}jUY+xb0 z88D+D%;$*FnLj2Haz04&Hghy}^swHFpU(=@ql;*{S@t;$U!&e^L_VerhRE)g6z>$} zR?OA~?I6-^5o4#d0Un}epU$;;4zYHoW`zrPbjIfU=we@v^pTwQ_xcHnJXU;!!U!cP zoXrU2?X7VcFJ5laTA5_GjU{_rdkPUWig+E&@ZBAwEP7g;lXl)!aR^P{k>pG=)tCVbzKzH)K;ScDz@_zj`$a2ohRl z!@HQ=-5k&wvt=vsof;q<6?z4~MnS)tN_nwzuiHgR*z_W=HiF!TJWe4Hn#g5KsUAg) zTlJ2!sug?iheqH~pnLKBg1#jsabs1r{!^!m%FxXRydBq}?b$3av=qhB@6fn*DE<4X zvOT!mA%Xs=@08N1ul-a8{?2F^Ss3Ok*@iJQu1`(6XR{x0DpCkII|aQcB`ytNsB!v^VvFmVAGF+64#{- zjENv#G7A2$xGk|=>}l>Ibw>K)E@k8$f+(p~Q2FzVoFE`l(qQTizjmS~jYCp8Lvy^i z7*Au4Okc4AMJ&Lcgg2nTZFF*}8{+$kjc$y-sXlBtX}ArStAH95Oc>UHSZOs!)>K8YBBuIFj6`p>)mbDZMSm8Clthtcst8Be=ptw^Tx!ei!RO3v6pgB z={1pFXFYd$FzzQr?vf=`;|m0q8o0^5h5@m}-~;{x;NOnURify_HRYNpjYJM!YIk+* zhW0n8Lu=z$IS(zdQQSJ@j`=dokMw7x@&_Nz%NQof(Uk|>FF&*IUH|ZA*QN2Jm{`TQ z&DnM7eonckAYG{RRIf2j`Fo7^gm!UP#>j*L9XAeD0}Qv9-7IQVjny@cLA$R_?mOgc z_fi8+P5a}*PX}3Ab%BL1f8a@kwRqlWaZUTa8Zl(O$@y(vsRTq`}B^R-Ad! z7{5}r7^-N5n0Um;pyhNf1&!l_q}2%HH?7sRb@euaIkldSwv$Epg+f)=-AaoGh!joF zgd8Uea-BkyB!^ZFaJI`Alx+Aapi+(x4-y#~o*##QNF?bcF%&$IDtWoca?0MQTvzqtCod9FQb z<+ut@TWfG%am27(4}6THwl+>DV@1XmiPC9!YgD|G#Ci8BbaaTST3bKIV{oFe9%ZOj zKU8oTY>dJfwLp4SBtr+8Bw-9J*>u7Ee+wI+d~NVxd#^K})Sjqd3F25()XpAt9_pR()h7$@-I(?;&H&d>}d+TqbHq=lLGPyPu=O;gg zT;E_ac5<385{(`3q}Kn_oG#qBk7sDQFFVX#igqO<{b)B-6s(#Xc~vIT667~P0p>nFm8YhNw=cXlN&sb&BlVW+X%Sd24OyuoeLY548?=8PWP-PUJU+PM75u4|;46#* zVVQPXXoaYOlFR0(;>uWwx378RtzB z&Q>?m&)c^U$?fc)8#?-92?)USeu4s4D}wh7W^U9EQrc9K^KMnjZ(n~R%af0bx8)4S z5>j4~E`HZMH5i%T32oZNn!c*@SL-{R1SRMPoY{gW~olu`A&RsI-?otHk`b|MC0)$v#3A5qv*vC`N~EqA7h2(^^aHzwWe7x zCm=C4NcXZ`N}?VHMCoaM*4|0-Us4zmuP95>t(zJ=o7$ZD$iN|*W8rmYBb+VlvgIn! zLpWnWDlLwCr{wGSPFBv!0z_BqE|cD@Pl)&d16N+=3_l$e1MI zNE=rjlnI+~fdK6`_*C%MqOhFtOe-s4HZ}~rmz9zl?^8QvtRE^)AsQ2Rxhy_)cRD27 zCT&i1ts~-`ouBQ$NF25sY5E)*n~!d|Z)^WyF?i2x>8|?D31z}p7ovFs+eivKYhlnu zcc~nI-Ksvi!2h{8q&_HWpj-GM`8`W!DSvmjt`e@BfhLgVb1*Hns`w|!q|SZ)1>I{F z>*reG;p7P6Q$}PVw3-k63?E@U5)T#$1!Wgmd1!>w3bjIw4e149^{a{!M?RUOH-AeX5?c6j)N zwJvzoh96d&_;^aXc!$TnkfnX#GT@Wx*PHjbBj?*SELx=|)eRN?hCwJzocSrf3+Oe& zVm%aA;u6&jq>3$$yv@IEYdY4GYze!7f zZoZJB*eJok;!Hq9J@z?bEQms;RJViZN1BdYjkoCr(JKDM-0QmMO&>r=5&XDUku_~a zyx%M|BaHDBT%I^$C|&Abq!oIHv==QIv++Xcqm&YIAlnGN@$A!*o_D3*k1`QN zX&Nq$-=}Bm_UqXK^WD;sB;wuF6S4YI|K^@fh~gKX1)qbV{#BXvrgVI~ZaF!OP(QB7 zD?@CGI`z@eIKXZw^z3Yf-@=`uwWUmZ`+|Fb^#@t}hrGy{%||DGsO=P<&R=uKo#$-C z)|7%)=sDb;tN7tk%9d)a8`?TkspHb{jRyiuHF(tl!m!3Y!DQA&I6$2T*h+#X!{O{2^=>m_{ZA89R@WIWRBQ@lCmn z96h-i?L}~RMyjWdHjQR@zUzM0n?D3KtR2zgJs}1%;!anLW1Br|%DoA3 zTyG+i_LmlXPk5+wM%St%cg|VpYoep$NPf`bPGP2!R@Z6l|0o>BwTQ2NF7VyYnPpdy zf(4 z+OG zRYiEL;(*cR<7c|=zUC07<{n(==M~qA?QBscl+eV62(WyU7>Rlru_XwSRJ11)SnWDb zzVDGsqjLkZaiLgGG-%GA&fCgZv6upFgB&xfn&Les)oqu!(4sZQ)~Iid*%o|#&R%SD z{eeFO93xYd7S}{vcl4zEOKbsmUSr-xnC)g?bpXD;C8@(q*=KqcT=jS1N2uM)9;x_) z=-Rpfg6s1$O?abk^6oziJo!IU4J*o%TDa!x52j0d<<54sPU(O^ks#=yreZ5R}_dY z6&f324>lm8MTq9=VkU3+YNZs5Ij^U*t>ESN%*jkO5(Fy5|COctKkt2@@kiQ+y&Vlf zKkEDWV|DmwdIXSg_04^*C}~tRH2V$WxSF_yl4LgQ>Gt>oUPA0y)sg&LSr1;2g2bzf z_%lnf|Hd2C&(Q^((WPn|@rVr$5o59$=a^q~3IC$R8zApP-dH0|hGKR4SJs?BpZ*lS z?wowGM=aqUIO5E8e`7$k3B4sGKi>x+QwpHH;1MMB)BmW!k0C4b7cW!*gI`epUMT;e zB{LPkyz(ywLI384LXXH4s{&RzPcrG4db@OE=2#(H(8{NZ-h$RYTgX?O&CHw3|A)Ii z{Hw_GzrV-+|JuUuT&&C)zyK-i3NNDT1o<4ee?cGrCqc3ks30TtZ*79`#Mwlb92iHA_tVdruxf!EB8qa zdzy7aW0s{<97z@$;73?!5T40)kWG=W4+%`=oBjB-NPniA1mUSEE<2qD=A{;f1DE4# zm#G`T(Af@8AzQlHZ(IHe(hwr^rQG50F8ykkG&!<^hnVQjnulA?{OL)pagiUggVcAf z=E+j%sy?Hf)ka$rP6}~W`Wk1$bS$Tu2$q9dq>wRWm$^l72ik$nBA+y%>WDDAZW-Qh@t2079?~ERXZSs|}8P z*0PVWLt#9kRx)F@Hf6bS%4TR67qQ<9Y8;2U#FLEr)*u5%hf%CxFgq*nQ?Hbf0zEB> z%2@-Zc$Y_cdIVVNj@9LihJ01y>C7G5&vNHqh89d>~`tlfHuB8m)%w(E4- z$Ecg$H*$$Edxan>(U>%{=DKiWkmtXX#LgAk(oE+dnb~o%WqjhJI$>Tb7$&bU9(9oCy z<8y3(OMAQsyB{DTMqBj>-ZC;X_ zWQ~$jR-=@d0TV+pkS+I8&q+}8U>UMo;#`*polZh-%R-~LT0>nd z?G@1*V$V`3OH*jrneKu_G3&w1K^0B6JRYk`rT4iE+DC_aLA1_rD%q%ZkGHv5S3YbU zYPupiapYWc--kEzWH7$QxTH&g>LwcwQQe5vBwK1CPl#>Vir;F~GzpcAl`j$_T)d}L zNh^Kvq(vHmmut_qgO?|RY$giLTqv^PN#vzUwQdH@=&W|s0@5@MvS9!%-4Pd;CF~NA zq6uMMo0&$9F6@Wy09TmH5IsGCUo&}J`(fx>Ni%Ax?#wp(fEn9l^1E3p39K?_^U9tC z7RF>eWXzM)Obzm=l+bBkBqfSgC=viDraNzrn)4MKazGpH-l7#Af4{sH_jNTj9p(GQR(}+_hLw9QobtNYnFS0Kt) z*WmQWk8$`D^9%b~5&K?SLTwK2m zG%LKmXqLl;OO`^OT7BG>lQ-eGN-W*Q740;p?;p)6sXQaAaS`&g#JHrI<(#FlHK6T{ zNllpLZekW(Xd;G1<r_kWou#SqA7y21jLb|q z`GF?>UR=V>NlMkp$jS6~Z7;0t%#EI*aX$N99mnrePBc!=zt`>nHdZxtLem8rdM$~@ zp=RplgvRl^;+KDZzWVc122KAL1%G3U=O4&dbGI}7MXK5Fy@M&h%pYxVD43d97`+6$ z0WJI@f}ab>4g8E3sEXqiz%o$K5g?!UH$5CGrjB4|2V>J;m-zm;^upE_{JV(2ALmjI zU}wAEtf>OTIvCkH+8F`O8M_1Z0asp|x>y*Ssz|;3Md?2h`>Tateehev7S@170hwC^ zVtQ?A3^oC%mN&IEb20}S=KtLgaPObp=$2Zet^gi?hC{z=!-uOk zEa#xtyP1WrC3Z?@tp|t@eS@e5!q*lAibge_o6=v1%$%lYFb<)Gk5a?j#g9lEel~w3 zG|#P(d6ISE-tF=|E=SiNUHZk=#sZF3#JcH^uNu^cNaSA$Iy5nk&(#_7vaA`KjxF~lo;bcgkonZ{_zk2oCr;RW-v zi1%>(s)#3~L6YAQv^aH@5__(mZ<d~XWItg;N3k>7?rd*>^uYVe-k z{qSq!H)Ok}eKdhcIGtfpo&sIW_X>Khhg|eAWXAe|^iJ_ye8m*yFLiMnnMiypn-QMb zJ&4JEB_K9p$A@>ivOpVrt%aebYj{Gk-kqYf?qB?jfq;twa~5qExi^zBDYMcV#P=3m zyUoC&gK5cN@3}`9+K2>2JA-MQ+}Bc$2G6}U^#Pe#vd=9otv9w$tSp9m9z*vtSRQcZ z5@f=_-4_^io{>x32wGQ9v^R9dfV({~F0?P7cn%K-RP{0miW)FCQ9* zw5f%exf2?X0Ov1lvj#h;+8G%GMgFcO94{@L9N(BayaL+E4BYwS$O{TAo){Seop^f2I0s`%1DA9JOqIr0B_LGInXN*G9nT(5+X7( z5;6)3@*^}HG&EFHGy*Ja44lUV#KeyYh=@oj=&4A_Xvv9)s9C9L85o~3KP91Jd&b7} zjGpN!)2~V%prD|jJwn4rL&IkxB_d_|55Io4gK!@`SV3q;ct8Vsi2DEm_rcFj5EVct z(u3bG(4Vgd4-o)*Q68b9p#ueKa6k_qARs(ML_k79L_c_GQIaLY0vtI_`H4{-|g~kBLc0=@}TAo-*_B^6?7@3cY$QAt@y-Bde;W zuA!-=tz&Fr`rgdk!qU;n*~Qh(-6P;*V9=-FkkHt;_=Loy&&iOi?3~;@XnsLqWmRL>iXsvUk^YCf3OAY z|6uHY;tLnx>mec{0wT&Uz8*Yu1vUg+M5HI2$apW5QH<>IX}CT-B6t~-S@9K>mRseB z@SVd58WA1O3jOgf)_!yL?_25Ga-a#)iz7z%vdxQ^Uc05LoTvmW(f<9&f}7v zRZI2f9GlntVkM4Ry?}Kg;G97+Jww+^iqFi+(fe@&z?CQ6nv=>(~yc^E6J#Y*R>B|SIGi?6aabn)Aos#K)s=VkqU zA4OeaRw+>hlbtrq*16RvJaYgMu}QpQmFs%x{cu|3oak-BrTF zHOPlZs?;1Jad>gZS0|JuYgijQ9sB*`aXr8MLzy1RpsMBh)%`x z)RbG%XFk=v!ghu3lvhuw?uO#;`l?`Wp-BxtD=gBuvvoBB8%EYJQ)lh9sgoIvrWJ?P}HhdM7(&(%~#`- z%4jamLPwKvJZ88*HllGR&tl{f07!wA#GPMeaCPp?dy%kQ)Rc%Ra=``w>T`XkrCaud zoS^Cda50hqKm;(_*1ueN!rP4D&O2gaS8nOl$KoM}#m)ng6Vz1ZHat&Qaj3gDhY{EE zu$9xRMG5bH(H~CCjU9`Kg~GfTm)&Z>m;B!lMT$q zH0t}CY(Zf=_iL>!iokbw?|<;Xglw=iM_@<^wPjXYyp`dswh!Yz%mtBql7a!0&*d5b zi!^0KpRa0p;g$4Jdh#lIatZ0c8^a1Z>u!l&#!aN>aQq*TwiOOSEejzpB9;|4ZjH8B z)kdr8Q|%Ks2|YXCp{H{if!^%)YMQxT=11_9ysILmx*sJ-ErT14B9ojH-vo)5LBjak z;w8=(=|n%OqdvW57~8C_Zazi|Ov-R|7-9Kr>P#mYE-CpV8|tX)>pW1mEmq)quY{ac z=S8v-wnVh|N`B0=E_Atj)VBV;di!24B0M3fK)VlW$*-YGU}2N%rkB|tn-dUzv6}LO zpG8>$ybY&(46)kfzpR+n3YxAIN@8DZSL>H$-k*#hnM`8ibum=d)q#J!k*-Z(r){ii zAPIFMM%fkCn7H(ZIbFwqYv$bWB#_X%PVtu_7K9ujNgAk(-b29~S0S>>%2zhtn))>@ zH3E$+*}q(xeQOo7Xj`P=eV1CL8j8(ZB3MGF@-08uCgrr8T2wG!uDXk4lUvnUoDV5V zwNOs-Z!qri-a`s(s34O=vn&g{j%S)i9R44;=VmQyFG<5u8(=SSeEhaEeMqsj;-5&WNv%o>#7`1EGY_jQ);o&3M~UR5-P3;0prFLQ684|mYXWyCc^A%`ws?1#UvF7 zlrDI4eK+4=Mn_yjs)LswNbFdPTjF# z29-c}8Wk(dO7s_^q5Dz}v_#)Z_fIoM8$uyNyQ}@1!YP6qb@kyJ^dbFR8E>R$id